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 { towers } from './projects/towers';
import { CV, Email, GitHubLink, LinkedIn } from './shared'; import { CV, Email, GitHubLink, LinkedIn } from './shared';
const imageViewer = new ImageViewer();
const main = new Main( const main = new Main(
new Header({ new Header({
name: 'András Schmelczer', name: 'András Schmelczer',
image: me, image: me,
imageAltText: 'a picture of me', imageAltText: 'a picture of me',
imageViewer,
about: [ 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.', '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, platformGame,
photos, photos,
leds, leds,
].map((p) => new TimelineElement(p, 'Show details', 'Show less')), ].map((p) => new TimelineElement(p, 'Show details', 'Show less', imageViewer)),
Contact({ Contact({
title: 'Get in touch', title: 'Get in touch',
@ -72,6 +74,6 @@ const main = new Main(
export const portfolio: Array<PageElement> = [ export const portfolio: Array<PageElement> = [
main, main,
new ImageViewer(),
new UpArrowButton(main, 'go up'), 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import adAstraPoster from '../media/ad_astra.jpg'; import adAstraPoster from '../media/ad_astra.jpg';
import adAstraMp4 from '../media/mp4/ad_astra.mp4'; import adAstraMp4 from '../media/mp4/ad_astra.mp4';
import adAstraWebM from '../media/webm/ad_astra.webm'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import avoidPoster from '../media/avoid.png'; import avoidPoster from '../media/avoid.png';
import { Open } from '../shared'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import citySimulationMp4 from '../media/mp4/simulation.mp4'; import citySimulationMp4 from '../media/mp4/simulation.mp4';
import citySimulationPoster from '../media/simulation.jpg'; import citySimulationPoster from '../media/simulation.jpg';
import citySimulationWebM from '../media/webm/simulation.webm'; 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.', description: 'I simulated a city where car crashes are more frequent than usual.',
more: [ 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.', '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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import colorsPoster from '../media/color.jpg'; import colorsPoster from '../media/color.jpg';
export const colors: TimelineElementParameters = { export const colors: TimelineElementParameters = {
title: 'Photo colour grader', title: 'Photo colour grader',
date: '2018 June', date: '2018 June',
figure: Image({ figure: new BorderedImage({
image: colorsPoster, image: colorsPoster,
alt: 'a picture of the app', alt: 'a picture of the app',
container: true,
}), }),
description: 'An innovative (at least I thought so) colour grader web application.', description: 'An innovative (at least I thought so) colour grader web application.',
more: [ 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import declaredPoster from '../media/decla-red.png'; import declaredPoster from '../media/decla-red.png';
import bscThesis from '../media/sdf2d-andras-schmelczer.pdf'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import forexPoster from '../media/forex.jpg'; import forexPoster from '../media/forex.jpg';
import forexMp4 from '../media/mp4/forex.mp4'; import forexMp4 from '../media/mp4/forex.mp4';
import forexWebM from '../media/webm/forex.webm'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import mscThesis from '../media/great-ai-andras-schmelczer.pdf'; import mscThesis from '../media/great-ai-andras-schmelczer.pdf';
import greatAiPoster from '../media/great-ai.png'; import greatAiPoster from '../media/great-ai.png';
@ -7,10 +7,9 @@ import { Open, PyPi, Thesis } from '../shared';
export const greatAi: TimelineElementParameters = { export const greatAi: TimelineElementParameters = {
title: 'GreatAI &mdash; AI deployment framework', title: 'GreatAI &mdash; AI deployment framework',
date: '2022', date: '2022',
figure: Image({ figure: new BorderedImage({
image: greatAiPoster, image: greatAiPoster,
alt: 'some example code using GreatAI', alt: 'some example code using GreatAI',
container: true,
isEagerLoaded: true, isEagerLoaded: true,
}), }),
description: description:

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import ledPoster from '../media/led.jpg'; import ledPoster from '../media/led.jpg';
import ledMp4 from '../media/mp4/led.mp4'; import ledMp4 from '../media/mp4/led.mp4';
import ledWebM from '../media/webm/led.webm'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import myNotesPoster from '../media/my-notes.png'; import myNotesPoster from '../media/my-notes.png';
import { GitHub } from '../shared'; import { GitHub } from '../shared';
@ -6,10 +6,9 @@ import { GitHub } from '../shared';
export const myNotes: TimelineElementParameters = { export const myNotes: TimelineElementParameters = {
title: 'My Notes &mdash; Android app', title: 'My Notes &mdash; Android app',
date: '2019 November', date: '2019 November',
figure: Image({ figure: new BorderedImage({
image: myNotesPoster, image: myNotesPoster,
alt: 'two screenshots of the application', alt: 'two screenshots of the application',
container: true,
}), }),
description: 'A minimalist Android note organiser and editor powered by Markwon.', description: 'A minimalist Android note organiser and editor powered by Markwon.',
more: [ 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import nuclearEditorPoster from '../media/process-simulator-input.jpg'; import nuclearEditorPoster from '../media/process-simulator-input.jpg';
export const nuclearEditor: TimelineElementParameters = { export const nuclearEditor: TimelineElementParameters = {
title: 'Graph editor &mdash; JavaFX', title: 'Graph editor &mdash; JavaFX',
date: '2018 October - November', date: '2018 October - November',
figure: Image({ figure: new BorderedImage({
image: nuclearEditorPoster, image: nuclearEditorPoster,
alt: "a picture of the simulator's UI", alt: "a picture of the simulator's UI",
container: true,
}), }),
description: description:
'An intuitive editor to create and edit input for the nuclear facility simulator (see above).', '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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import processSimulatorPoster from '../media/process-simulator.jpg'; import processSimulatorPoster from '../media/process-simulator.jpg';
export const nuclear: TimelineElementParameters = { export const nuclear: TimelineElementParameters = {
title: 'Simulating the cooling system of a nuclear facility', title: 'Simulating the cooling system of a nuclear facility',
date: '2018 October - November', date: '2018 October - November',
figure: Image({ figure: new BorderedImage({
image: processSimulatorPoster, image: processSimulatorPoster,
alt: 'a screenshot of the simulator', alt: 'a screenshot of the simulator',
container: true,
}), }),
description: 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.', '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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import photosPoster from '../media/photos.jpg'; import photosPoster from '../media/photos.jpg';
import { Open } from '../shared'; import { Open } from '../shared';
@ -6,10 +6,9 @@ import { Open } from '../shared';
export const photos: TimelineElementParameters = { export const photos: TimelineElementParameters = {
title: 'Photos', title: 'Photos',
date: '2016 summer', date: '2016 summer',
figure: Image({ figure: new BorderedImage({
image: photosPoster, image: photosPoster,
alt: 'a picture of the website', alt: 'a picture of the website',
container: true,
}), }),
description: 'A simple webpage where you can view my photos.', description: 'A simple webpage where you can view my photos.',
more: [ more: [

View file

@ -1,5 +1,5 @@
import { Video } from '../../page/figure/video/video';
import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import { Video } from '../../page/video/video';
import platformMp4 from '../media/mp4/platform.mp4'; import platformMp4 from '../media/mp4/platform.mp4';
import platformPoster from '../media/platform.png'; import platformPoster from '../media/platform.png';
import platformWebM from '../media/webm/platform.webm'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import sdf2dPoster from '../media/sdf2d.png'; import sdf2dPoster from '../media/sdf2d.png';
import { NPM, Open, Youtube } from '../shared'; 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 { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters';
import towersPoster from '../media/towers.png'; import towersPoster from '../media/towers.png';
import { GitHub, Open } from '../shared'; import { GitHub, Open } from '../shared';
@ -6,10 +6,9 @@ import { GitHub, Open } from '../shared';
export const towers: TimelineElementParameters = { export const towers: TimelineElementParameters = {
title: 'Multi-device life tracking', title: 'Multi-device life tracking',
date: '2019 August - September', date: '2019 August - September',
figure: Image({ figure: new BorderedImage({
image: towersPoster, image: towersPoster,
alt: 'a picture of the website', alt: 'a picture of the website',
container: true,
}), }),
description: 'An aesthetic representation of your previous and current goals/tasks.', description: 'An aesthetic representation of your previous and current goals/tasks.',
more: [ more: [

View file

@ -25,15 +25,6 @@ html[animations='off'] {
} }
} }
button {
border: none;
background: none;
}
a {
text-decoration: none;
}
html { html {
height: 100%; 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 { svg {
stroke: var(--normal-text-color); stroke: var(--normal-text-color);
} }
@ -82,68 +90,8 @@ p {
@include main-font(); @include main-font();
} }
noscript { a {
@include square(100%); text-decoration: none;
@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;
} }
:focus { :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 { .figure-container {
position: relative; > .overlay {
.overlay {
@include square(100%); @include square(100%);
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
pointer-events: none;
iframe { > .loading {
position: absolute;
left: 0;
}
.loading {
@include square(var(--large-icon-size)); @include square(var(--large-icon-size));
@include absolute-center; @include absolute-center;
visibility: hidden; visibility: hidden;
} }
iframe { > iframe {
@include square(100%); @include square(100%);
border: none; border: none;
&:fullscreen { position: absolute;
border-radius: 0; left: 0;
}
} }
} }
&.loaded { &.loaded {
.figure-container, > .start-button {
.start-button {
visibility: hidden; visibility: hidden;
} }
.loading { > .overlay {
visibility: visible; 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 { ResponsiveImage } from '../../../types/responsive-image';
import { url } from '../../types/url'; import { url } from '../../../types/url';
export interface VideoParameters { export interface VideoParameters {
mp4: url; 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 { VideoParameters } from './video-parameters';
import { generate } from './video.html'; import { generate } from './video.html';
export class Video extends PageElement { export class Video extends Figure {
public constructor(options: VideoParameters) { public constructor(options: VideoParameters) {
super(generate(options)); super(generate(options), {
hasButton: true,
this.query('.start-button').addEventListener('click', this.startVideo.bind(this)); invertButton: options.invertButton,
});
} }
private async startVideo() { protected async onClick() {
this.query('.start-button').style.visibility = 'hidden'; this.query('.start-button').style.visibility = 'hidden';
this.htmlRoot.classList.add('loaded'); this.htmlRoot.classList.add('loaded');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,18 +5,6 @@ export class ImageViewer extends PageElement {
public constructor() { public constructor() {
super(generate()); 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) => { document.body.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
this.hideImage(); this.hideImage();
@ -26,11 +14,7 @@ export class ImageViewer extends PageElement {
this.htmlRoot.addEventListener('click', this.hideImage.bind(this)); this.htmlRoot.addEventListener('click', this.hideImage.bind(this));
} }
private showImage(source: HTMLImageElement) { public showImage(source: HTMLImageElement) {
if (source.attributes['image-viewer-ignore'] as boolean | undefined) {
return;
}
const image = this.query('img') as HTMLImageElement; const image = this.query('img') as HTMLImageElement;
image.src = ''; image.src = '';
image.src = source.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 { .image {
overflow: hidden; overflow: hidden;
position: relative;
z-index: -1;
img { img {
max-width: 100%; max-width: 100%;
max-height: 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 { html } from '../../types/html';
import { Preview } from '../preview/preview'; import { Figure } from '../figure/figure';
import { Video } from '../video/video';
export interface TimelineElementParameters { export interface TimelineElementParameters {
date: string; date: string;
figure: html | Video | Preview; figure: Figure;
title: string; title: string;
description: string; description: string;
more?: Array<html>; more?: Array<html>;

View file

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

View file

@ -1,4 +1,6 @@
import { titleToFragment } from '../../helper/title-to-fragment'; 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 { PageElement } from '../page-element';
import { TimelineElementParameters } from './timeline-element-parameters'; import { TimelineElementParameters } from './timeline-element-parameters';
import { generate } from './timeline-element.html'; import { generate } from './timeline-element.html';
@ -10,7 +12,8 @@ export class TimelineElement extends PageElement {
public constructor( public constructor(
private timelineElement: TimelineElementParameters, private timelineElement: TimelineElementParameters,
private readonly showMore: string, private readonly showMore: string,
private readonly showLess: string private readonly showLess: string,
imageViewer?: ImageViewer
) { ) {
super(generate(timelineElement, showMore)); super(generate(timelineElement, showMore));
@ -24,12 +27,11 @@ export class TimelineElement extends PageElement {
); );
} }
this.attachElementByReplacing( if (timelineElement.figure instanceof BorderedImage) {
'.figure', timelineElement.figure.imageViewer = imageViewer;
timelineElement.figure instanceof PageElement }
? timelineElement.figure
: new PageElement(timelineElement.figure) this.attachElementByReplacing('.figure', timelineElement.figure);
);
} }
protected initialize(): void { 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; --blurred-card-color: transparent;
--blur-radius: 16px; --blur-radius: 16px;
--special-text-color: var(--accent-color); --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 { @include on-large-screen {
@ -23,7 +24,6 @@
--normal-margin: 45px; --normal-margin: 45px;
--small-margin: 25px; --small-margin: 25px;
--shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.2); --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; --icon-size: 45px;
--large-icon-size: 60px; --large-icon-size: 60px;
--body-width: min(80%, 900px); --body-width: min(80%, 900px);
@ -37,7 +37,6 @@
--normal-margin: 30px; --normal-margin: 30px;
--small-margin: 15px; --small-margin: 15px;
--shadow: 0 0 10px 2px rgba(0, 0, 0, 0.075), 0 0 1px rgba(0, 0, 0, 0.125); --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; --icon-size: 35px;
--large-icon-size: 55px; --large-icon-size: 55px;
--body-width: 90%; --body-width: 90%;
@ -51,5 +50,5 @@
--blurred-card-color: #212f4a77; --blurred-card-color: #212f4a77;
--blur-radius: 30px; --blur-radius: 30px;
--special-text-color: #ffffff; --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 <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 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" /> z" />

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 814 B

Before After
Before After