feat: portal animation on share page (#401)

* feat: share link portal animation

* progress on the animation

* small fixes
This commit is contained in:
Erick
2023-01-20 16:31:20 -03:00
committed by GitHub
parent 220d5a0967
commit eea9e0a0f1
5 changed files with 308 additions and 62 deletions

View File

@ -9,7 +9,8 @@ import notFoundTmpl from './templates/notfound';
import shareTmpl from './templates/share';
import footerTmpl from './templates/footer';
import stylesTmpl from './templates/styles';
import scriptsTmpl from './templates/scripts';
import videoPlayerControllerTmpl from './templates/video-player-controller';
import portalAnimationControllerTmpl from './templates/portal-animation-controller';
import gaTmpl from './templates/ga';
@ -37,6 +38,8 @@ const BaseHTMLContext: Record<string, string | Record<string, string>> = {
playerVolume: bucketPathForFile('public/player-volume.png'),
playerFullscreen: bucketPathForFile('public/player-fullscreen.png'),
close: bucketPathForFile('public/close.png'),
desktopPortalSpritesheet: bucketPathForFile('public/desktop-portal-spritesheet.png'),
mobilePortalSpritesheet: bucketPathForFile('public/mobile-portal-spritesheet.png'),
},
meta: {
title: 'Flutter Forward Holobooth',
@ -48,7 +51,8 @@ const BaseHTMLContext: Record<string, string | Record<string, string>> = {
},
ga: gaTmpl,
styles: '',
scripts: '',
videoPlayerController: '',
portalAnimationController: '',
footer: '',
};
@ -76,7 +80,8 @@ function renderTemplate(
tmpl: string, context: Record<string, string | Record<string, string>>
): string {
context.styles = mustache.render(stylesTmpl, context);
context.scripts = mustache.render(scriptsTmpl, context);
context.videoPlayerController = mustache.render(videoPlayerControllerTmpl, context);
context.portalAnimationController = mustache.render(portalAnimationControllerTmpl, context);
context.footer = mustache.render(footerTmpl, context);
return mustache.render(tmpl, context);
}

View File

@ -0,0 +1,293 @@
export default `
(function() {
function MiniGameLoop(canvas, container, animation, [frameReference]) {
let lastUpdated;
let canvasContext;
let scale = 1;
this.objects = [];
const landscape = document.body.offsetWidth > document.body.offsetHeight;
this.render = function() {
this.objects.forEach(function(obj) {
obj.render(canvasContext);
});
}
this.update = function() {
const now = Date.now();
const delta = (now - lastUpdated) / 1000;
lastUpdated = now;
this.objects.forEach(function(obj) {
obj.update(delta);
});
requestAnimationFrame(() => {
this.update();
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
canvasContext.save();
canvasContext.scale(scale, scale);
this.render(canvasContext);
canvasContext.restore();
});
}
this.addObject = async function(obj) {
await obj.load();
this.objects.push(obj);
}
this.load = async function() {
this.objects.push(animation);
lastUpdated = Date.now();
canvasContext = canvas.getContext("2d");
await Promise.all(this.objects.map((o) => o.load(canvas)));
const observer = new ResizeObserver((_) => {
this.fixBounds();
});
observer.observe(container);
this.update();
}
this.fixBounds = function() {
const animationSize = animation.size();
const [
animationW,
animationH,
] = animationSize;
const aspect = animationW / animationH;
let canvasW;
let canvasH;
if (landscape) {
canvasW = container.offsetWidth * .75;
canvasH = canvasW * aspect;
} else {
canvasH = container.offsetHeight * .75;
canvasW = canvasH * aspect;
}
canvas.width = canvasW;
canvas.height = canvasH;
const scaleX = canvas.width / animationW;
const scaleY = canvas.height / animationH;
scale = Math.min(scaleX, scaleY);
}
}
const portalMode = {
portrait: {
texturePath: '{{{assetUrls.mobilePortalSpritesheet}}}',
textureSize: [650, 850],
thumbSize: [322, 378],
thumbOffset: [168, 104],
},
landscape: {
texturePath: '{{{assetUrls.desktopPortalSpritesheet}}}',
textureSize: [710, 750],
thumbSize: [498, 280],
thumbOffset: [100, 96],
},
};
function Sprite({
spritePath,
position = [0, 0],
size,
}) {
let image;
let loaded = false;
let x = position[0]; y = position[1];
let renderOffset, renderSize;
this.load = function(canvas) {
return new Promise((resolve, reject) => {
if (image && loaded) {
resolve();
} else {
image = new Image();
image.src = spritePath;
image.onload = function() {
const [thumbWidth, thumbHeight] = size;
const rateX = thumbWidth / image.width;
const rateY = thumbHeight / image.height;
const rate = Math.max(rateX, rateY);
renderSize = [image.width * rate, image.height * rate];
const [renderWidth, renderHeight] = renderSize;
renderOffset = [
(thumbWidth / 2) - renderWidth / 2,
(thumbHeight / 2) - renderHeight / 2,
];
resolve();
}
}
});
};
this.update = function(_) {}
this.render = function(canvas) {
const [thumbWidth, thumbHeight] = size;
const [renderWidth,renderHeight] = renderSize;
const [offsetX, offsetY] = renderOffset;
canvas.save();
canvas.clip(canvas.rect(x, y, thumbWidth, thumbHeight));
canvas.drawImage(
image,
0, // sX
0, // sY
image.width, // sWidth
image.height, // sHeight
x + offsetX, // dX
y + offsetY, // dY
renderWidth, // dWidth
renderHeight, // dHeight
);
canvas.restore();
}
}
function SpriteAnimation({
texturePath,
textureSize,
stepTime,
onComplete,
}) {
let image;
const frames = [];
let currentFrame;
let currentTime = 0;
let completed = false;
let x = 0, y = 0;
this.size = function() {
return textureSize;
}
this.load = function(canvas) {
return new Promise((resolve, reject) => {
image = new Image();
image.src = texturePath;
image.onload = function() {
const [textureWidth, textureHeight] = textureSize;
const columns = image.width / textureWidth;
const rows = image.height / textureHeight;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
frames.push({
y: y * textureHeight,
x: x * textureWidth,
textureWidth,
textureHeight,
});
}
}
currentFrame = frames[0];
resolve();
}
});
};
this.render = function(canvas) {
if (currentFrame) {
canvas.drawImage(
image,
currentFrame.x, // sx
currentFrame.y, // sy
currentFrame.textureWidth, // sWidth
currentFrame.textureHeight, // sHeight
x, // dx
y, // dy
currentFrame.textureWidth, // dWidth
currentFrame.textureHeight, // dHeight
);
}
};
this.update = function(dt) {
if (currentFrame && !completed) {
currentTime += dt;
if (currentTime >= stepTime) {
currentTime -= stepTime;
const i = frames.indexOf(currentFrame);
if (i + 1 < frames.length) {
currentFrame = frames[i + 1];
} else {
completed = true;
onComplete();
}
}
}
};
}
const portalCanvas = document.getElementById('portal-animation');
const container = document.querySelector('.share-video');
const mode = document.body.offsetWidth > document.body.offsetHeight
? portalMode.landscape
: portalMode.portrait;
const animation = new SpriteAnimation({
texturePath: mode.texturePath,
textureSize: mode.textureSize,
stepTime: 0.05,
onComplete: onComplete,
});
const game = new MiniGameLoop(
portalCanvas,
container,
animation,
mode.textureSize,
);
const thumb = new Sprite({
spritePath: '{{{thumbImageUrl}}}',
position: mode.thumbOffset,
size: mode.thumbSize,
});
// Pre load the thumb
thumb.load();
function onComplete() {
game.addObject(thumb);
}
game.load();
})();
`;

View File

@ -31,12 +31,7 @@ export default `
<div class="backdrop"></div>
<main>
<div class="share-video">
<img class="holocard" src={{assetUrls.holocard}} />
<div class="card-frame">
<div class="video-clip">
<video src="{{{shareVideoUrl}}}"></video>
</div>
<img src={{assetUrls.videoFrame}} />
<canvas id="portal-animation"></canvas>
</div>
</div>
<div class="info">
@ -102,7 +97,8 @@ export default `
</div>
</body>
<script type="text/javascript">
{{{scripts}}}
{{{videoPlayerController}}}
{{{portalAnimationController}}}
</script>
</html>
`;

View File

@ -39,7 +39,9 @@ main {
.share-video {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.info {
@ -127,38 +129,6 @@ h2 {
color: #C0C0C0;
}
.card-frame {
width: 368px;
height: 467px;
position: absolute;
bottom: 206px;
left: 50%;
transform: translate(-50%, 0);
cursor: pointer;
}
.card-frame img {
width: 100%;
position: absolute;
}
.card-frame .video-clip {
height: 318px;
width: 286px;
top: 48px;
left: 50%;
position: absolute;
transform: translate(-50%, 0);
clip-path: inset(0);
}
.card-frame .video-clip video {
height: 318px;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
}
.holocard {
width: 708px;
height: 716px;
@ -447,24 +417,6 @@ footer .right a {
height: 464px;
}
.card-frame {
width: 218px;
height: 217px;
position: absolute;
bottom: 256px;
}
.card-frame .video-clip {
height: 218px;
width: 186px;
top: 14px;
}
.card-frame .video-clip video {
height: 318px;
position: absolute;
}
.holocard {
width: 608px;
height: 616px;

View File

@ -1,6 +1,6 @@
export default `
(function() {
const frame = document.querySelector('.card-frame');
const frame = document.querySelector('#portal-animation');
const playButton = document.querySelector('.play-button');
const pauseButton = document.querySelector('.pause-button');
const closeButton = document.querySelector('.close-button');