<div id="nav">
<div id="content">
<h1>Full-Page Navigation</h1>
<div>
<a data-color="#2196F3" style="color: #2196F3" href="">Blue Page</a>
<a data-color="#F44336" style="color: #F44336" href="">Red Page</a>
<a data-color="#8BC34A" style="color: #8BC34A" href="">Green Page</a>
<a data-color="#FF9800" style="color: #FF9800" href="">Orange Page</a>
</div>
</div>
<svg id="rope" xmlns="http://www.w3.org/2000/svg">
<path fill="transparent" stroke-width="10px" stroke-linejoin="round"/>
<circle stroke-width="10px" r="20" id="handle"></circle>
</svg>
</div>
<div id="page">
<h1>Pull</h1>
</div>
html{
height: 100%;
}
body{
font-family: Arial;
margin: 0px;
height: 100%;
--color: #2196F3;
--bgColor: #424242;
color: var(--color);
background-color: var(--bgColor);
}
*{
touch-callout: none; /* iOS Safari */
user-select: none; /* Safari */
user-select: none; /* Konqueror HTML */
user-select: none; /* Old versions of Firefox */
user-select: none; /* Internet Explorer/Edge */
user-select: none;
}
#nav{
position: fixed;
transform: translateY(-70vh);
overflow: visible;
}
canvas{
display: none;
}
#nav > #content{
box-sizing: border-box;
margin: 0px;
padding: 20vh 10px 10vh 10px;
height: 70vh;
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
background-color: #616161;
}
#nav > #content > h1{
color: #eee;
margin: 0px;
text-align: center;
}
#nav > #content > div{
margin: 30px auto;
display: flex;
justify-content: center;
align-items: center;
}
#nav > #content a{
margin: 10px;
font-size: 20px;
}
#nav > #rope{
width: 100vw;
height: 100vh;
display: block;
}
#nav > #rope > circle{
fill: var(--bgColor);
stroke: var(--color);
cursor: pointer;
tap-highlight-color: transparent;
}
#nav > #rope > path{
stroke: var(--color);
}
#page{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: var(--textColor);
}
#page > h1{
font-size: 50px;
}
class PhysicsEngine{
constructor(){
this.navclosedPos = -30;
this.navHeight = 50;
this.canvasWidth = 500;
this.canvasHeight = 600;
this.initWorld();
}
initWorld(){
const engine = Matter.Engine.create();
const render = Matter.Render.create({
element: document.body,
engine: engine
});
let bodies = this.createBodies();
let constraints = this.createConstraints();
Matter.World.add(engine.world, [bodies, constraints]);
Matter.Engine.run(engine);
Matter.Render.run(render);
}
createBodies(){
this.nav = Matter.Bodies.rectangle(this.canvasWidth/2, this.navclosedPos, this.canvasWidth, this.navHeight, {isSensor: true, inertia: Infinity, mass: 0.1});
this.rope = this.createRope();
this.handle = this.rope.bodies[this.rope.bodies.length-1];
return [this.nav, this.rope];
}
createRope(){
const ropeParts = Matter.Body.nextGroup(true);
const rope = Matter.Composites.stack(this.canvasWidth/2, this.navclosedPos, 8, 1, -30, 0, (x, y) => {
return Matter.Bodies.circle(x, y, 15, {
collisionFilter: { group: ropeParts }
});
});
Matter.Composites.chain(rope, 0, 0.2, 0, -0.2, {stiffness: 1, damping: 0.6, length: 3});
return rope;
}
createConstraints(){
this.fixMenuToTop = Matter.Constraint.create({
bodyA: this.nav,
pointA: { x: 0, y: this.navHeight/2},
pointB: { x: this.canvasWidth/2, y: this.navclosedPos},
stiffness: 0.5,
damping: 0.1,
length: 0
})
this.fixMenuToBottom = Matter.Constraint.create({
bodyA: this.nav,
pointA: { x: 0, y: this.navHeight/2},
pointB: { x: this.canvasWidth/2, y: 0},
stiffness: 0.01,
damping: 0.1,
length: 0
})
const fixRopeToMenu = Matter.Constraint.create({
bodyA: this.nav,
pointA: { x: 0, y: this.navHeight-20 },
bodyB: this.rope.bodies[0],
stiffness: 1,
length: 0
})
this.fixMouseToHandle = Matter.Constraint.create({
bodyA: this.handle,
pointB:{x: 0, y: 0},
stiffness: 0.000000000000001,
length: 0
})
return [this.fixMenuToTop, this.fixMenuToBottom, fixRopeToMenu, this.fixMouseToHandle];
}
grabHandle(x,y){
this.moveHandle(x,y);
this.fixMouseToHandle.stiffness = 1;
}
moveHandle(x,y){
this.fixMouseToHandle.pointB.x = x;
this.fixMouseToHandle.pointB.y = y;
}
releaseHandle(){
this.fixMouseToHandle.stiffness = 0.000000000000001;
}
}
class Nav{
constructor(){
this.physicsEngine = new PhysicsEngine();
this.canvasWidth = this.physicsEngine.canvasWidth;
this.canvasHeight = this.physicsEngine.canvasHeight;
this.navElm = document.getElementById('content');
this.ropeContainer = document.getElementById('rope');
this.ropeElm = this.ropeContainer.querySelector('path');
this.handleElm = document.getElementById('handle');
this.pullText = document.querySelector('#page > h1');
for(let link of Array.from(document.querySelectorAll('a'))){
link.addEventListener('click', e =>{
e.preventDefault();
this.navigateTo(link.getAttribute('data-color'));
})
}
this.handleElm.addEventListener('mousedown', this.grab.bind(this));
document.body.addEventListener('mousemove', this.move.bind(this));
document.body.addEventListener('mouseup', this.release.bind(this));
this.handleElm.addEventListener('touchstart', this.grab.bind(this));
document.body.addEventListener('touchmove', this.move.bind(this), {passive: false});
document.body.addEventListener('touchend', this.release.bind(this));
this.grabbed = false;
this.isOpen = false;
this.shouldOpen = false;
this.inTransition = false;
this.onResize();
window.addEventListener('resize', this.onResize.bind(this));
this.render();
}
onResize(){
this.width = window.innerWidth;
this.height = window.innerHeight;
this.ropeContainer.setAttribute('viewport', `0 0 ${this.width} ${this.height}`);
this.navOpenPos = this.height/10*6;
this.physicsEngine.fixMenuToBottom.pointB.y = this.getCanvasY(this.navOpenPos);
}
grab(e){
let x = e.clientX || e.touches[0].clientX;
let y = e.clientY || e.touches[0].clientY;
this.grabbed = true;
this.physicsEngine.grabHandle(this.getCanvasX(x), this.getCanvasY(y));
this.inTransition = false;
console.log(this.grabbed);
}
move(e){
e.preventDefault();
let x = e.clientX || e.touches[0].clientX;
let y = e.clientY || e.touches[0].clientY;
let navPos = this.getScreenY(this.physicsEngine.nav.position.y + this.physicsEngine.navHeight/2)
if(navPos >= 40 && !this.isOpen && !this.inTransition){
this.release();
this.open();
}
else if((navPos >= this.navOpenPos + 25 || navPos <= this.navOpenPos - 10) && this.isOpen && !this.inTransition){
this.release();
this.close();
}
else if(this.grabbed)
this.physicsEngine.moveHandle(this.getCanvasX(x), this.getCanvasY(y));
}
release(){
if(this.grabbed){
this.physicsEngine.releaseHandle();
this.grabbed = false;
}
}
open(){
this.shouldOpen = true;
this.inTransition = true;
}
close(){
this.shouldOpen = false;
this.inTransition = true;
}
render(){
window.requestAnimationFrame(this.render.bind(this));
if(this.shouldOpen && !this.isOpen){
if(this.physicsEngine.fixMenuToTop.stiffness >= 0.01){
this.physicsEngine.fixMenuToTop.stiffness -= 0.02;
this.physicsEngine.fixMenuToBottom.stiffness += 0.02;
}
else
this.isOpen = true;
}
if(!this.shouldOpen && this.isOpen){
if(this.physicsEngine.fixMenuToTop.stiffness <= 0.5){
this.physicsEngine.fixMenuToTop.stiffness += 0.03;
this.physicsEngine.fixMenuToBottom.stiffness -= 0.03;
}
else
this.isOpen = false;
}
let path = `M ${this.width/2} ${this.getScreenY(this.physicsEngine.nav.position.y)}`;
for(let body of this.physicsEngine.rope.bodies){
path += `L ${this.getScreenX(body.position.x)} ${this.getScreenY(body.position.y)}`;
}
let lastBody = this.physicsEngine.rope.bodies[this.physicsEngine.rope.bodies.length - 1];
this.handleElm.setAttribute('cx', this.getScreenX(lastBody.position.x));
this.handleElm.setAttribute('cy', this.getScreenY(lastBody.position.y));
this.ropeElm.setAttribute('d', path);
//console.log(this.physicsEngine.nav.position.y);
this.navElm.style.transform = `translate(${0}px, ${this.getScreenY(this.physicsEngine.nav.position.y + this.physicsEngine.navHeight/2)}px)`;
}
getScreenX(canvasX){
return canvasX / this.canvasWidth * this.width;
}
getScreenY(canvasY){
return canvasY / this.canvasHeight * this.height;
}
getCanvasX(screenX){
return screenX / this.width * this.canvasWidth;
}
getCanvasY(screenY){
return screenY / this.height * this.canvasHeight;
}
navigateTo(page){
document.body.style.setProperty('--color', page);
this.close();
}
}
const nav = new Nav();
This Pen doesn't use any external CSS resources.