Improve image handling & fix shadows
This commit is contained in:
parent
bc5074b28d
commit
2bb2117a59
47 changed files with 330 additions and 329 deletions
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 — AI deployment framework',
|
||||
date: '2022',
|
||||
figure: Image({
|
||||
figure: new BorderedImage({
|
||||
image: greatAiPoster,
|
||||
alt: 'some example code using GreatAI',
|
||||
container: true,
|
||||
isEagerLoaded: true,
|
||||
}),
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 — 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: [
|
||||
|
|
|
|||
|
|
@ -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 — 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).',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
0
src/page/figure/bordered-image/bordered-image.scss
Normal file
0
src/page/figure/bordered-image/bordered-image.scss
Normal file
23
src/page/figure/bordered-image/bordered-image.ts
Normal file
23
src/page/figure/bordered-image/bordered-image.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
21
src/page/figure/figure.html.ts
Normal file
21
src/page/figure/figure.html.ts
Normal 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>`;
|
||||
29
src/page/figure/figure.scss
Normal file
29
src/page/figure/figure.scss
Normal 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
21
src/page/figure/figure.ts
Normal 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;
|
||||
}
|
||||
21
src/page/figure/preview/preview.html.ts
Normal file
21
src/page/figure/preview/preview.html.ts
Normal 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>`;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/page/figure/preview/preview.ts
Normal file
28
src/page/figure/preview/preview.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
14
src/page/figure/video/video.html.ts
Normal file
14
src/page/figure/video/video.html.ts
Normal 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>`;
|
||||
16
src/page/figure/video/video.scss
Normal file
16
src/page/figure/video/video.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>' : ''}
|
||||
`;
|
||||
31
src/page/image/image.html.ts
Normal file
31
src/page/image/image.html.ts
Normal 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>`;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
@use '../../style/mixins' as *;
|
||||
|
||||
.video-container {
|
||||
&.loaded > .start-button,
|
||||
&:not(.loaded) > video {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
Loading…
Add table
Add a link
Reference in a new issue