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

@ -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 {
visibility: visible;
> .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 {
> .image {
border-radius: 100%;
box-shadow: var(--shadow);
margin: auto;
> .profile-picture {
position: relative;
z-index: 1;
.figure-container {
&,
> .image {
border-radius: 100%;
}
}
}

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 {
max-width: 80%;
max-height: 80%;
}
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;
}
}