From eea9e0a0f179e23fbd6a1419bc9cd39d6dd7f28d Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 20 Jan 2023 16:31:20 -0300 Subject: [PATCH] feat: portal animation on share page (#401) * feat: share link portal animation * progress on the animation * small fixes --- functions/src/share/index.ts | 11 +- .../templates/portal-animation-controller.ts | 293 ++++++++++++++++++ functions/src/share/templates/share.ts | 10 +- functions/src/share/templates/styles.ts | 54 +--- ...{scripts.ts => video-player-controller.ts} | 2 +- 5 files changed, 308 insertions(+), 62 deletions(-) create mode 100644 functions/src/share/templates/portal-animation-controller.ts rename functions/src/share/templates/{scripts.ts => video-player-controller.ts} (97%) diff --git a/functions/src/share/index.ts b/functions/src/share/index.ts index 39cd0434..f7afcbfa 100644 --- a/functions/src/share/index.ts +++ b/functions/src/share/index.ts @@ -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> = { 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> = { }, ga: gaTmpl, styles: '', - scripts: '', + videoPlayerController: '', + portalAnimationController: '', footer: '', }; @@ -76,7 +80,8 @@ function renderTemplate( tmpl: string, context: Record> ): 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); } diff --git a/functions/src/share/templates/portal-animation-controller.ts b/functions/src/share/templates/portal-animation-controller.ts new file mode 100644 index 00000000..b1bb64ac --- /dev/null +++ b/functions/src/share/templates/portal-animation-controller.ts @@ -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(); + +})(); +`; diff --git a/functions/src/share/templates/share.ts b/functions/src/share/templates/share.ts index 3bb85ce5..fa03ebcb 100644 --- a/functions/src/share/templates/share.ts +++ b/functions/src/share/templates/share.ts @@ -31,12 +31,7 @@ export default `
@@ -102,7 +97,8 @@ export default `
`; diff --git a/functions/src/share/templates/styles.ts b/functions/src/share/templates/styles.ts index e70026e6..35ae6f4d 100644 --- a/functions/src/share/templates/styles.ts +++ b/functions/src/share/templates/styles.ts @@ -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; diff --git a/functions/src/share/templates/scripts.ts b/functions/src/share/templates/video-player-controller.ts similarity index 97% rename from functions/src/share/templates/scripts.ts rename to functions/src/share/templates/video-player-controller.ts index af5f4ed6..ba34b0b1 100644 --- a/functions/src/share/templates/scripts.ts +++ b/functions/src/share/templates/video-player-controller.ts @@ -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');