Improve image handling & fix shadows

This commit is contained in:
Andras Schmelczer 2022-09-28 14:18:17 +02:00
parent bc5074b28d
commit 2bb2117a59
No known key found for this signature in database
GPG key ID: 0EA1BC97D0AB076E
47 changed files with 330 additions and 329 deletions

View file

@ -24,11 +24,13 @@ import { sdf2d } from './projects/sdf2d';
import { towers } from './projects/towers';
import { CV, Email, GitHubLink, LinkedIn } from './shared';
const imageViewer = new ImageViewer();
const main = new Main(
new Header({
name: 'András Schmelczer',
image: me,
imageAltText: 'a picture of me',
imageViewer,
about: [
'With more than six years of professional software engineering experience and a degree in Computer Science, I can confidently undertake any challenge. My interests span diverse areas, allowing me to design complex — even multidisciplinary — systems with a clear understanding.',
@ -56,7 +58,7 @@ const main = new Main(
platformGame,
photos,
leds,
].map((p) => new TimelineElement(p, 'Show details', 'Show less')),
].map((p) => new TimelineElement(p, 'Show details', 'Show less', imageViewer)),
Contact({
title: 'Get in touch',
@ -72,6 +74,6 @@ const main = new Main(
export const portfolio: Array<PageElement> = [
main,
new ImageViewer(),
new UpArrowButton(main, 'go up'),
imageViewer,
];

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import adAstraPoster from '../media/ad_astra.jpg';
import adAstraMp4 from '../media/mp4/ad_astra.mp4';
import adAstraWebM from '../media/webm/ad_astra.webm';

View file

@ -1,4 +1,4 @@
import { Preview } from '../../page/preview/preview';
import { Preview } from '../../page/figure/preview/preview';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import avoidPoster from '../media/avoid.png';
import { Open } from '../shared';

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import citySimulationMp4 from '../media/mp4/simulation.mp4';
import citySimulationPoster from '../media/simulation.jpg';
import citySimulationWebM from '../media/webm/simulation.webm';
@ -16,7 +16,7 @@ export const citySimulation: TimelineElementParameters = {
}),
description: 'I simulated a city where car crashes are more frequent than usual.',
more: [
'The state of the traffic lights can be changed through a REST API. Drivers follow the instructions of the traffic lights, so if a mistake is made, there will be collisions. There is also support for displaying tweets on a HUD. This was created as the context for a cybersecurity challenge on PLCs. With the help of this program, the contestants could instantly see the effect of their work.',
'The state of the traffic lights can be changed through a REST API. Drivers follow the instructions of the traffic lights, so if a mistake is made, there will be collisions. There is also support for displaying tweets on a HUD. This was created as the context for a cybersecurity challenge about PLCs. With the help of this program, the contestants could instantly see the effect of their work.',
'An exciting aspect of the project was building it in a server-client architecture. Every decision of the agents is calculated server-side. The real challenge was broadcasting these decisions in a fault-tolerant way using minimal bandwidth.',

View file

@ -1,14 +1,13 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import colorsPoster from '../media/color.jpg';
export const colors: TimelineElementParameters = {
title: 'Photo colour grader',
date: '2018 June',
figure: Image({
figure: new BorderedImage({
image: colorsPoster,
alt: 'a picture of the app',
container: true,
}),
description: 'An innovative (at least I thought so) colour grader web application.',
more: [

View file

@ -1,4 +1,4 @@
import { Preview } from '../../page/preview/preview';
import { Preview } from '../../page/figure/preview/preview';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import declaredPoster from '../media/decla-red.png';
import bscThesis from '../media/sdf2d-andras-schmelczer.pdf';

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import forexPoster from '../media/forex.jpg';
import forexMp4 from '../media/mp4/forex.mp4';
import forexWebM from '../media/webm/forex.webm';

View file

@ -1,4 +1,4 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import mscThesis from '../media/great-ai-andras-schmelczer.pdf';
import greatAiPoster from '../media/great-ai.png';
@ -7,10 +7,9 @@ import { Open, PyPi, Thesis } from '../shared';
export const greatAi: TimelineElementParameters = {
title: 'GreatAI &mdash; AI deployment framework',
date: '2022',
figure: Image({
figure: new BorderedImage({
image: greatAiPoster,
alt: 'some example code using GreatAI',
container: true,
isEagerLoaded: true,
}),
description:

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import ledPoster from '../media/led.jpg';
import ledMp4 from '../media/mp4/led.mp4';
import ledWebM from '../media/webm/led.webm';

View file

@ -1,4 +1,4 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import myNotesPoster from '../media/my-notes.png';
import { GitHub } from '../shared';
@ -6,10 +6,9 @@ import { GitHub } from '../shared';
export const myNotes: TimelineElementParameters = {
title: 'My Notes &mdash; Android app',
date: '2019 November',
figure: Image({
figure: new BorderedImage({
image: myNotesPoster,
alt: 'two screenshots of the application',
container: true,
}),
description: 'A minimalist Android note organiser and editor powered by Markwon.',
more: [

View file

@ -1,14 +1,13 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import nuclearEditorPoster from '../media/process-simulator-input.jpg';
export const nuclearEditor: TimelineElementParameters = {
title: 'Graph editor &mdash; JavaFX',
date: '2018 October - November',
figure: Image({
figure: new BorderedImage({
image: nuclearEditorPoster,
alt: "a picture of the simulator's UI",
container: true,
}),
description:
'An intuitive editor to create and edit input for the nuclear facility simulator (see above).',

View file

@ -1,14 +1,13 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import processSimulatorPoster from '../media/process-simulator.jpg';
export const nuclear: TimelineElementParameters = {
title: 'Simulating the cooling system of a nuclear facility',
date: '2018 October - November',
figure: Image({
figure: new BorderedImage({
image: processSimulatorPoster,
alt: 'a screenshot of the simulator',
container: true,
}),
description:
'The temperatures and flow volumes are dynamically calculated by two graph models on a remote server while multiple "monitoring" clients update in real-time.',

View file

@ -1,4 +1,4 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import photosPoster from '../media/photos.jpg';
import { Open } from '../shared';
@ -6,10 +6,9 @@ import { Open } from '../shared';
export const photos: TimelineElementParameters = {
title: 'Photos',
date: '2016 summer',
figure: Image({
figure: new BorderedImage({
image: photosPoster,
alt: 'a picture of the website',
container: true,
}),
description: 'A simple webpage where you can view my photos.',
more: [

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import platformMp4 from '../media/mp4/platform.mp4';
import platformPoster from '../media/platform.png';
import platformWebM from '../media/webm/platform.webm';

View file

@ -1,4 +1,4 @@
import { Preview } from '../../page/preview/preview';
import { Preview } from '../../page/figure/preview/preview';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import sdf2dPoster from '../media/sdf2d.png';
import { NPM, Open, Youtube } from '../shared';

View file

@ -1,4 +1,4 @@
import { Image } from '../../page/image-viewer/image/image.html';
import { BorderedImage } from '../../page/figure/bordered-image/bordered-image';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import towersPoster from '../media/towers.png';
import { GitHub, Open } from '../shared';
@ -6,10 +6,9 @@ import { GitHub, Open } from '../shared';
export const towers: TimelineElementParameters = {
title: 'Multi-device life tracking',
date: '2019 August - September',
figure: Image({
figure: new BorderedImage({
image: towersPoster,
alt: 'a picture of the website',
container: true,
}),
description: 'An aesthetic representation of your previous and current goals/tasks.',
more: [

View file

@ -25,15 +25,6 @@ html[animations='off'] {
}
}
button {
border: none;
background: none;
}
a {
text-decoration: none;
}
html {
height: 100%;
@ -74,6 +65,23 @@ body {
}
}
noscript {
@include square(100%);
@include center-children();
@include sub-title-font();
}
.image,
video,
iframe {
user-select: none;
}
button {
border: none;
background: none;
}
svg {
stroke: var(--normal-text-color);
}
@ -82,68 +90,8 @@ p {
@include main-font();
}
noscript {
@include square(100%);
@include center-children();
@include sub-title-font();
}
.start-button {
@include image-button(var(--large-icon-size));
@include absolute-center;
@include square(calc(var(--large-icon-size) + var(--normal-margin) * 2));
// as a result of the firefox fix, it is required for iOS devices
transform: translate3d(-50%, -50%, 0.00001px);
&:hover svg {
box-shadow: var(--shadow);
}
svg {
border-radius: 10000px;
@include blurred-background;
transition: transform var(--transition-time), box-shadow var(--transition-time);
}
&.inverted svg {
fill: var(--accent-color);
}
}
.figure-container {
font-size: 0;
box-shadow: var(--inset-shadow);
border-radius: var(--border-radius) var(--border-radius) 0 0;
pointer-events: none;
position: relative;
* {
pointer-events: auto;
}
.image,
video,
iframe {
z-index: -1;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
}
video,
iframe {
// the picture of videos is not always visible on firefox mobile without this
transform: translate3d(0, 0, 0.00001px);
}
.image,
video,
iframe {
user-select: none;
a {
text-decoration: none;
}
:focus {

View file

@ -0,0 +1,23 @@
import { ResponsiveImage } from '../../../types/responsive-image';
import { ImageViewer } from '../../image-viewer/image-viewer';
import { Image } from '../../image/image.html';
import { Figure } from '../figure';
import './bordered-image.scss';
export class BorderedImage extends Figure {
public constructor(
options: {
image: ResponsiveImage;
alt: string;
sizes?: string | null;
isEagerLoaded?: boolean;
},
public imageViewer?: ImageViewer
) {
super(Image(options));
}
protected async onClick() {
this.imageViewer?.showImage(this.query('img') as HTMLImageElement);
}
}

View file

@ -0,0 +1,21 @@
import play from '../../../static/icons/play-button.svg';
import { html } from '../../types/html';
import './figure.scss';
export const generate = ({
children,
hasButton,
invertButton,
}: {
children: html;
hasButton: boolean;
invertButton: boolean;
}): html => `
<div class="figure-container" tabindex=0 >
${children}
${
hasButton
? `<div class="start-button ${invertButton ? 'inverted' : ''}" >${play}</div>`
: ''
}
</div>`;

View file

@ -0,0 +1,29 @@
@use '../../style/mixins' as *;
.figure-container {
box-shadow: var(--inset-shadow);
transition: box-shadow var(--transition-time);
position: relative;
cursor: pointer;
> .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: 1000px;
@include blurred-background;
transition: transform var(--transition-time), box-shadow var(--transition-time);
}
&.inverted > svg {
fill: var(--accent-color);
}
}
}

21
src/page/figure/figure.ts Normal file
View file

@ -0,0 +1,21 @@
import { html } from '../../types/html';
import { PageElement } from '../page-element';
import { generate } from './figure.html';
export abstract class Figure extends PageElement {
public constructor(
children: html,
{
hasButton = false,
invertButton = false,
}: {
hasButton?: boolean;
invertButton?: boolean;
} = {}
) {
super(generate({ children, hasButton, invertButton }));
this.htmlRoot.addEventListener('click', this.onClick.bind(this));
}
protected abstract onClick(): unknown;
}

View file

@ -0,0 +1,21 @@
import loading from '../../../../static/icons/loading.svg';
import { html } from '../../../types/html';
import { ResponsiveImage } from '../../../types/responsive-image';
import { Image } from '../../image/image.html';
import './preview.scss';
export const generate = ({
alt,
poster,
}: {
alt: string;
poster: ResponsiveImage;
}): html =>
`${Image({
image: poster,
alt,
})}
<div class="overlay">
<div class="loading">${loading}</div>
<iframe title="${alt}" allowfullscreen loading="lazy"></iframe>
</div>`;

View file

@ -1,42 +1,38 @@
@use '../../style/mixins' as *;
@use '../../../style/mixins' as *;
.preview {
position: relative;
.overlay {
.figure-container {
> .overlay {
@include square(100%);
position: absolute;
left: 0;
top: 0;
pointer-events: none;
iframe {
position: absolute;
left: 0;
}
.loading {
> .loading {
@include square(var(--large-icon-size));
@include absolute-center;
visibility: hidden;
}
iframe {
> iframe {
@include square(100%);
border: none;
&:fullscreen {
border-radius: 0;
}
position: absolute;
left: 0;
}
}
&.loaded {
.figure-container,
.start-button {
> .start-button {
visibility: hidden;
}
.loading {
> .overlay {
pointer-events: all;
> .loading {
visibility: visible;
}
}
}
}

View file

@ -0,0 +1,28 @@
import { ResponsiveImage } from '../../../types/responsive-image';
import { Figure } from '../figure';
import { generate } from './preview.html';
export class Preview extends Figure {
public constructor(poster: ResponsiveImage, private readonly url: string, alt: string) {
super(generate({ poster, alt }), {
hasButton: true,
});
this.url += '?portfolioView';
}
protected onClick() {
this.htmlRoot.classList.add('loaded');
(this.query('iframe') as HTMLIFrameElement).src = this.url;
}
protected initialize() {
new IntersectionObserver((e) => {
if (!e[0].isIntersecting) {
this.htmlRoot.classList.remove('loaded');
(this.query('iframe') as HTMLIFrameElement).src = '';
}
}).observe(this.htmlRoot.parentElement!);
super.initialize();
}
}

View file

@ -1,5 +1,5 @@
import { ResponsiveImage } from '../../types/responsive-image';
import { url } from '../../types/url';
import { ResponsiveImage } from '../../../types/responsive-image';
import { url } from '../../../types/url';
export interface VideoParameters {
mp4: url;

View file

@ -0,0 +1,14 @@
import { html } from '../../../types/html';
import { Image } from '../../image/image.html';
import { VideoParameters } from './video-parameters';
import './video.scss';
export const generate = ({ webm, mp4, poster, altText }: VideoParameters): html => `
${Image({
image: poster,
alt: altText,
})}
<video playsinline controls preload="none">
<source src="${webm}" type="video/webm"/>
<source src="${mp4}" type="video/mp4"/>
</video>`;

View file

@ -0,0 +1,16 @@
@use '../../../style/mixins' as *;
.figure-container {
&.loaded > .start-button,
&:not(.loaded) > video {
visibility: hidden;
}
> video {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
}

View file

@ -1,15 +1,16 @@
import { PageElement } from '../page-element';
import { Figure } from '../figure';
import { VideoParameters } from './video-parameters';
import { generate } from './video.html';
export class Video extends PageElement {
export class Video extends Figure {
public constructor(options: VideoParameters) {
super(generate(options));
this.query('.start-button').addEventListener('click', this.startVideo.bind(this));
super(generate(options), {
hasButton: true,
invertButton: options.invertButton,
});
}
private async startVideo() {
protected async onClick() {
this.query('.start-button').style.visibility = 'hidden';
this.htmlRoot.classList.add('loaded');

View file

@ -4,15 +4,13 @@ import './header.scss';
export const generate = ({
name,
about,
photo,
}: {
name: string;
about: Array<string>;
photo: html;
}): html => `
<header id="about">
<div class="photo-container">
${photo}
<div class="profile-picture">
<img/>
<div class="placeholder"></div>
</div>

View file

@ -12,8 +12,9 @@
}
$img-size: 125px;
> .photo-container > .image {
> .profile-picture {
@include square($img-size);
margin: auto;
}
> h1 {
@ -30,11 +31,10 @@
auto;
border-radius: var(--border-radius);
> .photo-container {
position: relative;
> .image {
> .profile-picture {
> .figure-container {
@include square($img-size);
position: absolute;
left: calc(#{math.div(-$img-size, 3)} - var(--normal-margin));
top: calc(#{math.div(-$img-size, 3)} - var(--normal-margin));
@ -55,15 +55,19 @@
}
> h1,
> .photo-container > .placeholder {
> .profile-picture > .placeholder {
@include title-font();
}
> .photo-container {
> .profile-picture {
position: relative;
z-index: 1;
.figure-container {
&,
> .image {
border-radius: 100%;
box-shadow: var(--shadow);
margin: auto;
}
}
}

View file

@ -1,5 +1,6 @@
import { ResponsiveImage } from '../../types/responsive-image';
import { Image } from '../image-viewer/image/image.html';
import { BorderedImage } from '../figure/bordered-image/bordered-image';
import { ImageViewer } from '../image-viewer/image-viewer';
import { PageElement } from '../page-element';
import { generate } from './header.html';
import { ThemeSwitcher } from './theme-switcher/theme-switcher';
@ -10,22 +11,31 @@ export class Header extends PageElement {
image,
imageAltText,
about,
imageViewer,
}: {
name: string;
image: ResponsiveImage;
imageAltText: string;
about: Array<string>;
imageViewer?: ImageViewer;
}) {
super(
generate({
name,
about,
photo: Image({
})
);
this.attachElementByReplacing(
'img',
new BorderedImage(
{
image,
alt: imageAltText,
sizes: '(max-width: 924px) 125px, 190px',
}),
})
},
imageViewer
)
);
this.attachElement(new ThemeSwitcher());
}

View file

@ -4,7 +4,7 @@ import './image-viewer.scss';
export const generate = (): html => `
<div id="image-viewer">
<img height="0" width="0" image-viewer-ignore />
<img height="0" width="0" />
<button id="cancel">${cancel}</button>
</div>
`;

View file

@ -13,11 +13,9 @@
img {
@include square(auto);
@include on-large-screen {
box-shadow: var(--shadow);
max-width: 80%;
max-height: 80%;
}
@include on-small-screen {
max-width: 95%;

View file

@ -5,18 +5,6 @@ export class ImageViewer extends PageElement {
public constructor() {
super(generate());
document.body.addEventListener('click', (event: MouseEvent) => {
const element = event.target as HTMLElement;
if (element.classList?.contains('image')) {
this.showImage(element.querySelector('img')!);
}
if (element instanceof HTMLImageElement) {
this.showImage(element);
}
});
document.body.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this.hideImage();
@ -26,11 +14,7 @@ export class ImageViewer extends PageElement {
this.htmlRoot.addEventListener('click', this.hideImage.bind(this));
}
private showImage(source: HTMLImageElement) {
if (source.attributes['image-viewer-ignore'] as boolean | undefined) {
return;
}
public showImage(source: HTMLImageElement) {
const image = this.query('img') as HTMLImageElement;
image.src = '';
image.src = source.src;

View file

@ -1,44 +0,0 @@
import { html } from '../../../types/html';
import { ResponsiveImage } from '../../../types/responsive-image';
import './image.scss';
export const Image = ({
image,
alt,
container = false,
isIgnoredByImageViewer = false,
sizes = null,
isEagerLoaded = false,
}: {
image: ResponsiveImage;
alt: string;
container?: boolean;
isIgnoredByImageViewer?: boolean;
sizes?: string | null;
isEagerLoaded?: boolean;
}): html => `
${
container
? `<div class="figure-container" style="padding-top:${
(image.height / image.width) * 100
}%">`
: ''
}
<div
class="image"
style="background-size: cover; background-image: url('${image.placeholder}')"
${isIgnoredByImageViewer ? '' : 'tabindex="0"'}
>
<img
${isIgnoredByImageViewer ? 'image-viewer-ignore' : ''}
${isEagerLoaded ? '' : 'loading="lazy"'}
srcset="${image.srcSet}"
${sizes ? `sizes="${sizes}"` : ''}
src="${image.src}"
width="${image.width}"
height="${image.height}"
alt="${alt}"
/>
</div>
${container ? '</div>' : ''}
`;

View file

@ -0,0 +1,31 @@
import { html } from '../../types/html';
import { ResponsiveImage } from '../../types/responsive-image';
import './image.scss';
export const Image = ({
image,
alt,
sizes = null,
isEagerLoaded = false,
}: {
image: ResponsiveImage;
alt: string;
sizes?: string | null;
isEagerLoaded?: boolean;
}): html => `
<div
class="image"
style="background-size: cover; background-image: url('${
image.placeholder
}'); aspect-ratio: ${image.width / image.height}"
>
<img
${isEagerLoaded ? '' : 'loading="lazy"'}
srcset="${image.srcSet}"
${sizes ? `sizes="${sizes}"` : ''}
src="${image.src}"
width="${image.width}"
height="${image.height}"
alt="${alt}"
/>
</div>`;

View file

@ -1,12 +1,10 @@
.image {
overflow: hidden;
position: relative;
z-index: -1;
img {
max-width: 100%;
max-height: 100%;
&:not([image-viewer-ignore]) {
cursor: pointer;
}
}
}

View file

@ -1,28 +0,0 @@
import loading from '../../../static/icons/loading.svg';
import play from '../../../static/icons/play-button.svg';
import { html } from '../../types/html';
import { ResponsiveImage } from '../../types/responsive-image';
import { Image } from '../image-viewer/image/image.html';
import './preview.scss';
export const generate = ({
alt,
poster,
}: {
alt: string;
poster: ResponsiveImage;
}): html => `
<div class="preview">
${Image({
image: poster,
alt,
container: true,
isIgnoredByImageViewer: true,
})}
<div class="overlay">
<div class="loading">${loading}</div>
<iframe title="${alt}" allowfullscreen loading="lazy"></iframe>
<div class="start-button">${play}</div>
</div>
</div>
`;

View file

@ -1,31 +0,0 @@
import { ResponsiveImage } from '../../types/responsive-image';
import { PageElement } from '../page-element';
import { generate } from './preview.html';
export class Preview extends PageElement {
public constructor(poster: ResponsiveImage, private readonly url: string, alt: string) {
super(generate({ poster, alt }));
this.url += '?portfolioView';
this.query('.start-button').addEventListener('click', this.loadContent.bind(this));
}
protected initialize() {
new IntersectionObserver((e) => {
if (!e[0].isIntersecting) {
this.unloadContent();
}
}).observe(this.htmlRoot.parentElement!);
super.initialize();
}
public loadContent() {
this.htmlRoot.classList.add('loaded');
(this.query('iframe') as HTMLIFrameElement).src = this.url;
}
public unloadContent() {
this.htmlRoot.classList.remove('loaded');
(this.query('iframe') as HTMLIFrameElement).src = '';
}
}

View file

@ -1,10 +1,9 @@
import { html } from '../../types/html';
import { Preview } from '../preview/preview';
import { Video } from '../video/video';
import { Figure } from '../figure/figure';
export interface TimelineElementParameters {
date: string;
figure: html | Video | Preview;
figure: Figure;
title: string;
description: string;
more?: Array<html>;

View file

@ -105,6 +105,10 @@
background-color: var(--blurred-card-color);
transition: background-color var(--transition-time);
> .figure-container {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
> .lower {
> * {
padding: 0 var(--normal-margin);

View file

@ -1,4 +1,6 @@
import { titleToFragment } from '../../helper/title-to-fragment';
import { BorderedImage } from '../figure/bordered-image/bordered-image';
import { ImageViewer } from '../image-viewer/image-viewer';
import { PageElement } from '../page-element';
import { TimelineElementParameters } from './timeline-element-parameters';
import { generate } from './timeline-element.html';
@ -10,7 +12,8 @@ export class TimelineElement extends PageElement {
public constructor(
private timelineElement: TimelineElementParameters,
private readonly showMore: string,
private readonly showLess: string
private readonly showLess: string,
imageViewer?: ImageViewer
) {
super(generate(timelineElement, showMore));
@ -24,12 +27,11 @@ export class TimelineElement extends PageElement {
);
}
this.attachElementByReplacing(
'.figure',
timelineElement.figure instanceof PageElement
? timelineElement.figure
: new PageElement(timelineElement.figure)
);
if (timelineElement.figure instanceof BorderedImage) {
timelineElement.figure.imageViewer = imageViewer;
}
this.attachElementByReplacing('.figure', timelineElement.figure);
}
protected initialize(): void {

View file

@ -1,28 +0,0 @@
import play from '../../../static/icons/play-button.svg';
import { html } from '../../types/html';
import { Image } from '../image-viewer/image/image.html';
import { VideoParameters } from './video-parameters';
import './video.scss';
export const generate = ({
webm,
mp4,
poster,
invertButton,
altText,
}: VideoParameters): html => `
<div class="figure-container video-container" style="padding-top:${
(poster.height / poster.width) * 100
}%">
${Image({
image: poster,
alt: altText,
isIgnoredByImageViewer: true,
})}
<video playsinline controls preload="none">
<source src="${webm}" type="video/webm"/>
<source src="${mp4}" type="video/mp4"/>
</video>
<div class="start-button ${invertButton ? 'inverted' : ''}" tabindex=0>${play}</div>
</div>
`;

View file

@ -1,8 +0,0 @@
@use '../../style/mixins' as *;
.video-container {
&.loaded > .start-button,
&:not(.loaded) > video {
visibility: hidden;
}
}

View file

@ -14,6 +14,7 @@
--blurred-card-color: transparent;
--blur-radius: 16px;
--special-text-color: var(--accent-color);
--inset-shadow: inset 0 0 4px 1px rgba(0, 0, 0, 0.05), inset 0 0 5px rgba(0, 0, 0, 0.2);
}
@include on-large-screen {
@ -23,7 +24,6 @@
--normal-margin: 45px;
--small-margin: 25px;
--shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.2);
--inset-shadow: inset 0 -9px 7px -7px rgb(0, 0, 0, 0.15);
--icon-size: 45px;
--large-icon-size: 60px;
--body-width: min(80%, 900px);
@ -37,7 +37,6 @@
--normal-margin: 30px;
--small-margin: 15px;
--shadow: 0 0 10px 2px rgba(0, 0, 0, 0.075), 0 0 1px rgba(0, 0, 0, 0.125);
--inset-shadow: inset 0 -9px 7px -7px rgb(0, 0, 0, 0.15);
--icon-size: 35px;
--large-icon-size: 55px;
--body-width: 90%;
@ -51,5 +50,5 @@
--blurred-card-color: #212f4a77;
--blur-radius: 30px;
--special-text-color: #ffffff;
--inset-shadow: inset 0 0 10px 2px rgba(0, 0, 0, 0.25), inset 0 0 1px rgba(0, 0, 0, 0.4);
--inset-shadow: inset 0 0 10px 2px rgba(0, 0, 0, 0.3), inset 0 0 4px rgba(0, 0, 0, 0.5);
}

View file

@ -1,4 +1,4 @@
<svg version="1.1" fill="#ffffff" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<svg version="1.1" fill="#ffffff" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path d="M256,0C114.833,0,0,114.844,0,256s114.833,256,256,256s256-114.844,256-256S397.167,0,256,0z M256,490.667
C126.604,490.667,21.333,385.396,21.333,256S126.604,21.333,256,21.333S490.667,126.604,490.667,256S385.396,490.667,256,490.667
z" />

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 814 B

Before After
Before After