Improve video UX
This commit is contained in:
parent
08feeb6bfc
commit
f3c8453782
13 changed files with 101 additions and 69 deletions
|
|
@ -1,16 +1,15 @@
|
||||||
import adAstraPoster from '../static/media/ad_astra.jpg?format=jpg';
|
import adAstraPoster from '../static/media/ad_astra.jpg?format=jpg';
|
||||||
import adAstraMp4 from '../static/media/ad_astra_720.mp4';
|
import adAstraMp4 from '../static/media/mp4/ad_astra.mp4';
|
||||||
import adAstraWebM from '../static/media/ad_astra_720.webm';
|
import adAstraWebM from '../static/media/webm/ad_astra.webm';
|
||||||
|
|
||||||
import { GitHub } from './shared';
|
import { GitHub } from './shared';
|
||||||
import { last } from '../helper/last';
|
|
||||||
import { Video } from '../page/basics/video/video';
|
import { Video } from '../page/basics/video/video';
|
||||||
|
|
||||||
export const adAstraTimelineElement = {
|
export const adAstraTimelineElement = {
|
||||||
title: `Gaming on an ATtiny85`,
|
title: `Gaming on an ATtiny85`,
|
||||||
date: `2020 Spring`,
|
date: `2020 Spring`,
|
||||||
figure: new Video({
|
figure: new Video({
|
||||||
poster: last(adAstraPoster.images)!.path,
|
poster: adAstraPoster,
|
||||||
mp4: adAstraMp4,
|
mp4: adAstraMp4,
|
||||||
webm: adAstraWebM,
|
webm: adAstraWebM,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import citySimulationPoster from '../static/media/simulation.jpg?format=jpg';
|
import citySimulationPoster from '../static/media/simulation.jpg?format=jpg';
|
||||||
import citySimulationWebM from '../static/media/simulation.webm';
|
import citySimulationMp4 from '../static/media/mp4/simulation.mp4';
|
||||||
import citySimulationMp4 from '../static/media/simulation.mp4';
|
import citySimulationWebM from '../static/media/webm/simulation.webm';
|
||||||
|
|
||||||
import { Video } from '../page/basics/video/video';
|
import { Video } from '../page/basics/video/video';
|
||||||
import { last } from '../helper/last';
|
|
||||||
|
|
||||||
export const citySimulationTimelineElement = {
|
export const citySimulationTimelineElement = {
|
||||||
date: `2018 July - August`,
|
date: `2018 July - August`,
|
||||||
title: `City simulation`,
|
title: `City simulation`,
|
||||||
figure: new Video({
|
figure: new Video({
|
||||||
poster: last(citySimulationPoster.images)!.path,
|
poster: citySimulationPoster,
|
||||||
mp4: citySimulationMp4,
|
mp4: citySimulationMp4,
|
||||||
webm: citySimulationWebM,
|
webm: citySimulationWebM,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import forexMp4 from '../static/media/forex.mp4';
|
import forexMp4 from '../static/media/mp4/forex.mp4';
|
||||||
import forexWebM from '../static/media/forex.webm';
|
import forexWebM from '../static/media/webm/forex.webm';
|
||||||
|
import forexPoster from '../static/media/forex.jpg';
|
||||||
|
|
||||||
import { Video } from '../page/basics/video/video';
|
import { Video } from '../page/basics/video/video';
|
||||||
|
|
||||||
export const forexTimelineElement = {
|
export const forexTimelineElement = {
|
||||||
title: `Predicting foreign exchange rates`,
|
title: `Predicting foreign exchange rates`,
|
||||||
date: `2019 Autumn`,
|
date: `2019 Autumn`,
|
||||||
figure: new Video({
|
figure: new Video({
|
||||||
|
poster: forexPoster,
|
||||||
mp4: forexMp4,
|
mp4: forexMp4,
|
||||||
webm: forexWebM,
|
webm: forexWebM,
|
||||||
shouldActLikeGif: true,
|
invertButton: true,
|
||||||
}),
|
}),
|
||||||
description: `
|
description: `
|
||||||
From the animation, we can see that my implementation does a somewhat acceptable job at
|
From the animation, we can see that my implementation does a somewhat acceptable job at
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import ledPoster from '../static/media/led.jpg?format=jpg';
|
import ledPoster from '../static/media/led.jpg?format=jpg';
|
||||||
import ledMp4 from '../static/media/led.mp4';
|
import ledMp4 from '../static/media/mp4/led.mp4';
|
||||||
import ledWebM from '../static/media/led.webm';
|
import ledWebM from '../static/media/webm/led.webm';
|
||||||
|
|
||||||
import { last } from '../helper/last';
|
|
||||||
import { Video } from '../page/basics/video/video';
|
import { Video } from '../page/basics/video/video';
|
||||||
|
|
||||||
export const ledsTimelineElement = {
|
export const ledsTimelineElement = {
|
||||||
date: `2016 spring`,
|
date: `2016 spring`,
|
||||||
title: `Lights synchronised to music`,
|
title: `Lights synchronised to music`,
|
||||||
figure: new Video({
|
figure: new Video({
|
||||||
poster: last(ledPoster.images)!.path,
|
poster: ledPoster,
|
||||||
mp4: ledMp4,
|
mp4: ledMp4,
|
||||||
webm: ledWebM,
|
webm: ledWebM,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
transition: background-color var(--transition-time);
|
transition: background-color var(--transition-time);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
|
||||||
&:nth-child(odd) {
|
&:nth-child(odd) {
|
||||||
background-color: #fff9e0;
|
background-color: #fff9e0;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,13 @@ export const generate = ({
|
||||||
alt: string;
|
alt: string;
|
||||||
container: boolean;
|
container: boolean;
|
||||||
}): html => `
|
}): html => `
|
||||||
${container ? `<div class="figure-container">` : ''}
|
${
|
||||||
|
container
|
||||||
|
? `<div class="figure-container" style="padding-top:${(imageJpeg.height /
|
||||||
|
imageJpeg.width) *
|
||||||
|
100}%">`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
<picture loading="lazy">
|
<picture loading="lazy">
|
||||||
<source
|
<source
|
||||||
srcset="${imageWebP.srcSet}"
|
srcset="${imageWebP.srcSet}"
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ export const generate = ({ alt }: { alt: string }): html => `
|
||||||
<img image-viewer-ignore class="poster" />
|
<img image-viewer-ignore class="poster" />
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<div class="loading">${loading}</div>
|
<div class="loading">${loading}</div>
|
||||||
<iframe title="${alt}" height=300 allowfullscreen loading="lazy"></iframe>
|
<iframe title="${alt}" allowfullscreen loading="lazy"></iframe>
|
||||||
<div class="load-button">${play}</div>
|
<div class="start-button">${play}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,9 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
* {
|
iframe {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.load-button {
|
|
||||||
@include image-button(var(--large-icon-size));
|
|
||||||
@include absolute-center;
|
|
||||||
@include square(calc(var(--large-icon-size) + var(--normal-margin) * 2));
|
|
||||||
|
|
||||||
&:hover svg {
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
border-radius: 10000px;
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
transition: transform var(--transition-time), box-shadow var(--transition-time);
|
|
||||||
stroke: var(--normal-text-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
|
|
@ -54,9 +36,10 @@
|
||||||
|
|
||||||
&.loaded {
|
&.loaded {
|
||||||
.figure-container,
|
.figure-container,
|
||||||
.load-button {
|
.start-button {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export class Preview extends PageElement {
|
||||||
super(createElement(generate({ alt })));
|
super(createElement(generate({ alt })));
|
||||||
this.url += '?portfolioView';
|
this.url += '?portfolioView';
|
||||||
this.attachElementByReplacing('.poster', new Image(posterWebP, posterJpeg, alt));
|
this.attachElementByReplacing('.poster', new Image(posterWebP, posterJpeg, alt));
|
||||||
this.query('.load-button').addEventListener('click', this.loadContent.bind(this));
|
this.query('.start-button').addEventListener('click', this.loadContent.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public setParent(parent: PageElement) {
|
public setParent(parent: PageElement) {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,17 @@
|
||||||
import './video.scss';
|
import './video.scss';
|
||||||
import { url } from '../../../types/url';
|
|
||||||
import { html } from '../../../types/html';
|
|
||||||
|
|
||||||
export const generate = ({
|
import { html } from '../../../types/html';
|
||||||
webm,
|
import play from '../../../static/icons/play-button.svg';
|
||||||
mp4,
|
import { VideoParameters } from './video';
|
||||||
poster,
|
import { last } from '../../../helper/last';
|
||||||
shouldActLikeGif,
|
|
||||||
container,
|
export const generate = ({ webm, mp4, poster, invertButton }: VideoParameters): html => `
|
||||||
}: {
|
<div class="figure-container" style="padding-top:${(poster.height / poster.width) *
|
||||||
webm: url;
|
100}%">
|
||||||
mp4: url;
|
<video playsinline preload="none" poster="${last(poster.images)!.path}">
|
||||||
poster?: url;
|
|
||||||
shouldActLikeGif?: boolean;
|
|
||||||
container?: boolean;
|
|
||||||
}): html => `
|
|
||||||
${container === undefined || container ? `<div class="figure-container">` : ''}
|
|
||||||
<video loading="lazy" ${
|
|
||||||
shouldActLikeGif ? 'autoplay loop muted' : ''
|
|
||||||
} controls playsinline preload="metadata" ${poster ? `poster="${poster}` : ''}" >
|
|
||||||
<source src="${webm}" type="video/webm"/>
|
<source src="${webm}" type="video/webm"/>
|
||||||
<source src="${mp4}" type="video/mp4"/>
|
<source src="${mp4}" type="video/mp4"/>
|
||||||
</video>
|
</video>
|
||||||
${container === undefined || container ? `</div>` : ''}
|
<div class="start-button ${invertButton ? 'inverted' : ''}">${play}</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@use '../../../style/mixins' as *;
|
||||||
|
|
@ -2,15 +2,40 @@ import { PageElement } from '../../page-element';
|
||||||
import { createElement } from '../../../helper/create-element';
|
import { createElement } from '../../../helper/create-element';
|
||||||
import { generate } from './video.html';
|
import { generate } from './video.html';
|
||||||
import { url } from '../../../types/url';
|
import { url } from '../../../types/url';
|
||||||
|
import { ResponsiveImage } from '../../../types/responsive-image';
|
||||||
|
|
||||||
|
export interface VideoParameters {
|
||||||
|
mp4: url;
|
||||||
|
webm: url;
|
||||||
|
poster: ResponsiveImage;
|
||||||
|
invertButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class Video extends PageElement {
|
export class Video extends PageElement {
|
||||||
public constructor(options: {
|
private video: HTMLVideoElement;
|
||||||
poster?: url;
|
|
||||||
mp4: url;
|
public constructor(options: VideoParameters) {
|
||||||
webm: url;
|
|
||||||
shouldActLikeGif?: boolean;
|
|
||||||
container?: boolean;
|
|
||||||
}) {
|
|
||||||
super(createElement(generate(options)));
|
super(createElement(generate(options)));
|
||||||
|
this.video = this.query('video') as HTMLVideoElement;
|
||||||
|
this.video.addEventListener('click', this.startVideo.bind(this));
|
||||||
|
this.query('.start-button').addEventListener('click', this.startVideo.bind(this));
|
||||||
|
this.video.addEventListener('pause', this.stopVideo.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private startVideo(e: Event) {
|
||||||
|
if (this.video.paused) {
|
||||||
|
this.query('.start-button').style.visibility = 'hidden';
|
||||||
|
this.video.play();
|
||||||
|
this.video.controls = true;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopVideo(e: Event) {
|
||||||
|
if (this.video.paused) {
|
||||||
|
this.query('.start-button').style.visibility = 'visible';
|
||||||
|
this.video.controls = false;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,26 @@ noscript {
|
||||||
@include center-children();
|
@include center-children();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.start-button {
|
||||||
|
@include image-button(var(--large-icon-size));
|
||||||
|
@include absolute-center;
|
||||||
|
@include square(calc(var(--large-icon-size) + var(--normal-margin) * 2));
|
||||||
|
|
||||||
|
&:hover svg {
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
border-radius: 10000px;
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: transform var(--transition-time), box-shadow var(--transition-time);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inverted svg {
|
||||||
|
fill: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.figure-container {
|
.figure-container {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
box-shadow: var(--inset-shadow);
|
box-shadow: var(--inset-shadow);
|
||||||
|
|
@ -59,14 +79,19 @@ noscript {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
* {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
img,
|
img,
|
||||||
video,
|
video,
|
||||||
iframe {
|
iframe {
|
||||||
pointer-events: auto;
|
|
||||||
position: relative;
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue