Demo
Code
- HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>iPhone X</title>
<style>
...
</style>
</head>
<body>
<div class="video-wrapper">
<video muted="" playsinline="" autoplay="" loop="" id="iphone-x" src="https://images.apple.com/media/us/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/overview/primary/hero/large_2x.mp4"></video>
</div>
<canvas id="cover-canvas"></canvas>
<script>
...
</script>
</body>
</html>
- CSS
<style>
/* Clear default style*/
html{
height:100%;
font-family:sans-serif;
-webkit-text-size-adjust:100%;
-ms-text-size-adjust:100%;
-webkit-tap-highlight-color:rgba(0,0,0,0)
}
body{
height:100%;
-webkit-font-smoothing:antialiased;
font-smoothing:antialiased;
-webkit-overflow-scrolling:touch;
overflow-scrolling:touch
}
html,body,div,span,applet,object,iframe,figure,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{
margin:0;
padding:0;
border:0
}
article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{
display:block
}
div,article,section,p,ul,li,span,label{
box-sizing:border-box
}
/**********************/
body{
background:#000
}
#cover-canvas{
position:fixed;
top:0;
left:0
}
.video-wrapper{
display:flex;
justify-content:center;
align-items:center;
overflow:hidden;
position:fixed;
top:0;
left:0;
width:100vw;
height:100vh
}
#video-studiomeal{
/* This video scale will be changed */
transform:scale(1)
}
</style>
- JavaScript
<script>
'use strict';
(function(){
var elemCanvas,
elemVideo,
elemPhone,
context,
windowWidth=0, // window width
windowHeight=0, // window height
canvasWidth=0, // canvas width
canvasHeight=0, // canvas height
scrollY=0, // current scroll pos
relativeScrollY=0, // relative scroll pos in each frame
prevDurations=0, // duration from prev keyframe
totalScrollHeight=0, // total height for scroll(body height)
currentKeyframe=0, // current keyframe (0, 1)
phoneWidth=4000, // iPhone image width
phoneHeight=4000, // iPhone image height
resizeHandler,
scrollHandler,
render,
drawCanvas,
calcAnimationValue,
calcFinalValue,
init,
pixelDuration=0, // scroll height of each keyframe
// there is 2 keyframes
// first keyframe: start
// second keyframe: change X to iPhone
keyframes=[
{
animationValues:{
videoScale:[1,2], // to be bigger
triangleMove:[0,200], // X
rectangleMove:[0,500]
}
},
{
animationValues:{
videoScale:[2,0.5], // to be smaller
triangleMove:[200,1000],
rectangleMove:[500,500]
}
}
],
// canvas
elemBody=document.body,
elemCanvas=document.getElementById('cover-canvas'),
context=elemCanvas.getContext('2d');
elemVideo=document.getElementById('iphone-x');
init=function(){
windowWidth=window.innerWidth;
windowHeight=window.innerHeight;
resizeHandler();
render();
// requestAnimationFrame is for smooth rendering
// because resize and scroll are frequent event.
window.addEventListener('resize',function(){
requestAnimationFrame(resizeHandler);
});
window.addEventListener('scroll',function(){
requestAnimationFrame(scrollHandler);
});
elemPhone=document.createElement('img');
elemPhone.src='phone.png';
elemPhone.addEventListener('load',function(){
drawCanvas();
});
};
resizeHandler=function(){
var i;
windowWidth=window.innerWidth;
windowHeight=window.innerHeight;
totalScrollHeight=0;
// one keyframe duration is half of window height
pixelDuration=0.5*windowHeight;
// totalScrollHeight = windowHeight
for(i=0;i<keyframes.length;i++){
totalScrollHeight+=pixelDuration;
}
totalScrollHeight+=windowHeight;
elemBody.style.height=totalScrollHeight+'px';
elemCanvas.width=canvasWidth=windowWidth*2;
elemCanvas.height=canvasHeight=windowHeight*2;
elemCanvas.style.width=windowWidth+'px';
elemCanvas.style.height=windowHeight+'px';
};
scrollHandler=function(){
scrollY=window.pageYOffset; // current scroll pos
// scroll range valid check
if(scrollY<0||scrollY>(totalScrollHeight-windowHeight)){
return;
}
// when scroll down
if(scrollY>pixelDuration+prevDurations){
prevDurations+=pixelDuration;
currentKeyframe++;
}
// when scroll up
else if(scrollY<prevDurations){
currentKeyframe--;
prevDurations-=pixelDuration;
}
// current keyframe scroll pos
relativeScrollY=scrollY-prevDurations;
render();
};
render=function(){
var videoScale,
triangleMove,
rectangleMove;
if(keyframes[currentKeyframe]){
videoScale=calcAnimationValue(keyframes[currentKeyframe].animationValues.videoScale);
triangleMove=calcAnimationValue(keyframes[currentKeyframe].animationValues.triangleMove);
rectangleMove=calcAnimationValue(keyframes[currentKeyframe].animationValues.rectangleMove);
}
else{
return;
}
elemVideo.style.transform='scale('+videoScale+')';
// clear canvas every time before drawing
context.clearRect(0,0,canvasWidth,canvasHeight);
if(elemPhone){
drawCanvas(videoScale,triangleMove,rectangleMove);
}
};
calcAnimationValue=function(values){
// current keyframe scroll pos / keyframe scroll = ratio
// values[1]-values[0] is diff value
// values[0] is init value
return(relativeScrollY/pixelDuration)*(values[1]-values[0])+values[0];
};
drawCanvas=function(videoScale,triangleMove,rectangleMove){
var videoScale=videoScale||1,
triangleMove=triangleMove||0,
rectangleMove=rectangleMove||0;
context.save();
context.translate((canvasWidth-phoneWidth*videoScale)*0.5,(canvasHeight-phoneHeight*videoScale)*0.5);
// phone image scale is changed by video scale
context.drawImage(elemPhone,0,0,phoneWidth*videoScale,phoneHeight*videoScale);
context.restore();
context.fillStyle='black';
// top triangle
context.beginPath();
context.moveTo(canvasWidth*0.5-1500,-triangleMove-1700);
context.lineTo(canvasWidth*0.5,canvasHeight*0.5-150-triangleMove);
context.lineTo(canvasWidth*0.5+1500,-triangleMove-1700);
context.lineTo(canvasWidth*0.5-1500,-triangleMove-1700);
context.fill();
context.closePath();
// bottom triangle
context.beginPath();
context.moveTo(canvasWidth*0.5-1500,canvasHeight+triangleMove+1700);
context.lineTo(canvasWidth*0.5,canvasHeight*0.5+150+triangleMove);
context.lineTo(canvasWidth*0.5+1500,canvasHeight+triangleMove+1700);
context.lineTo(canvasWidth*0.5-1500,canvasHeight+triangleMove+1700);
context.fill();
context.closePath();
// left triangle
context.beginPath();
context.moveTo(canvasWidth*0.5-1700-triangleMove,-1700);
context.lineTo(canvasWidth*0.5-130-triangleMove,canvasHeight*0.5);
context.lineTo(canvasWidth*0.5-1700-triangleMove,canvasHeight+1700);
context.lineTo(canvasWidth*0.5-1700-triangleMove,-1700);
context.fill();
context.closePath();
// right triangle
context.beginPath();
context.moveTo(canvasWidth*0.5+1700+triangleMove,-1700);
context.lineTo(canvasWidth*0.5+130+triangleMove,canvasHeight*0.5);
context.lineTo(canvasWidth*0.5+1700+triangleMove,canvasHeight+1700);
context.lineTo(canvasWidth*0.5+1700+triangleMove,-1700);
context.fill();
context.closePath();
// Box top, bottom
context.fillRect(0,canvasHeight*0.5-2600-rectangleMove,canvasWidth,2000);
context.fillRect(0,canvasHeight*0.5+600+rectangleMove,canvasWidth,2000);
};
init();
})();
</script>