Copy everything
22
src/helper/create-image.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { ResponsiveImage } from '../model/responsive-image';
|
||||
import { last } from './last';
|
||||
|
||||
export const createImage = (
|
||||
image: ResponsiveImage,
|
||||
alt: string,
|
||||
tabIndex: number,
|
||||
imageScreenRatio = 0.25
|
||||
): HTMLImageElement => {
|
||||
const img = new Image(image.width, image.height);
|
||||
|
||||
img.tabIndex = tabIndex;
|
||||
img.srcset = image.srcSet;
|
||||
img.sizes =
|
||||
image.images
|
||||
.map(d => `(max-width: ${d.width / imageScreenRatio}px) ${d.width}px,`)
|
||||
.join('\n') + `\n${last(image.images).width}px`;
|
||||
img.src = last(image.images)?.path;
|
||||
img.alt = alt;
|
||||
|
||||
return img;
|
||||
};
|
||||
2
src/helper/last.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const last = <T>(list: ArrayLike<T>): T =>
|
||||
list.length >= 1 ? list[list.length - 1] : undefined;
|
||||
24
src/index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Photos - András Schmelczer</title>
|
||||
<meta name="description" content="A very simple page where I showcase some of my photos. Although I am not a professional photographer, I truly love capturing joyous moments.">
|
||||
<meta name="theme-color" content="#A63446">
|
||||
<meta name="viewport" content="initial-scale=1.0" />
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>András Schmelczer</h1>
|
||||
</header>
|
||||
<main id="images">
|
||||
<section id="portraits" class="cluster"></section>
|
||||
<div id="frame-container">
|
||||
<img src="" alt="opened photo" id="frame-image"/>
|
||||
</div>
|
||||
<section id="landscapes" class="cluster"></section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
239
src/index.scss
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
@mixin center-children() {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin portrait() {
|
||||
@media (max-width: 900px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin landscape() {
|
||||
@media (min-width: 900px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
$text-color: #ffffff;
|
||||
$accent-color: #a63446;
|
||||
$background-color: #eeeeee;
|
||||
$border-width: 5px;
|
||||
$normal-margin: 24px;
|
||||
$small-margin: 6px;
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: $accent-color solid $border-width * 1.1;
|
||||
outline-offset: -$border-width * 1.1;
|
||||
}
|
||||
|
||||
img {
|
||||
border: $border-width solid $text-color;
|
||||
box-sizing: content-box;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
background-color: $background-color;
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
@include portrait {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@include landscape {
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
width: 12px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $accent-color;
|
||||
border-radius: 1000px;
|
||||
}
|
||||
}
|
||||
h1,
|
||||
header {
|
||||
font-family: 'Roboto', serif;
|
||||
font-weight: 100;
|
||||
|
||||
@include landscape {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
@include portrait {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
@include center-children;
|
||||
box-sizing: content-box;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
@include portrait {
|
||||
width: 100%;
|
||||
height: 2.15ch;
|
||||
padding: $normal-margin 0;
|
||||
}
|
||||
|
||||
@include landscape {
|
||||
width: 2.15ch;
|
||||
height: 100vh;
|
||||
padding: 0 $normal-margin;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $text-color;
|
||||
background-color: $accent-color;
|
||||
padding: $normal-margin 0;
|
||||
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
z-index: 2;
|
||||
position: fixed;
|
||||
|
||||
text-align: center;
|
||||
|
||||
@include portrait {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include landscape {
|
||||
width: 101vh;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main#images {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@include portrait {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cluster {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&#portraits > img {
|
||||
@include portrait {
|
||||
height: 12vmin;
|
||||
width: auto;
|
||||
}
|
||||
@include landscape {
|
||||
width: 8vmin;
|
||||
}
|
||||
}
|
||||
|
||||
&#landscapes > img {
|
||||
@include portrait {
|
||||
height: 8vmin;
|
||||
width: auto;
|
||||
}
|
||||
@include landscape {
|
||||
width: 12vmin;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include portrait {
|
||||
margin: 0 $small-margin;
|
||||
}
|
||||
}
|
||||
|
||||
@include portrait {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
margin: $small-margin 0;
|
||||
padding: $small-margin 0;
|
||||
object-fit: contain;
|
||||
|
||||
:first-child {
|
||||
margin-left: 2 * $small-margin;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 2 * $small-margin !important;
|
||||
}
|
||||
}
|
||||
|
||||
@include landscape {
|
||||
flex-direction: column;
|
||||
margin: $normal-margin;
|
||||
|
||||
img {
|
||||
margin: $small-margin 0;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#frame-container {
|
||||
@include center-children;
|
||||
margin: 0 $border-width;
|
||||
|
||||
flex: 1;
|
||||
@include landscape {
|
||||
}
|
||||
|
||||
@include portrait {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
|
||||
@include portrait {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
max-width: 100% !important; // undo js
|
||||
max-height: 100%;
|
||||
left: 50% !important; // undo js
|
||||
}
|
||||
|
||||
@include landscape {
|
||||
position: fixed;
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import './index.scss';
|
||||
import { Photos } from './photos';
|
||||
|
||||
// @ts-ignore
|
||||
const images = require.context('./pictures', true, /.jpg$/);
|
||||
const imagePath = name => images(name, true);
|
||||
|
||||
const addSupportForTabNavigation = () =>
|
||||
(document.onkeydown = e => {
|
||||
if (e.key === ' ') {
|
||||
(document.activeElement as HTMLElement)?.click();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const sizeFrame = () => {
|
||||
const container = document.querySelector('#frame-container') as HTMLElement;
|
||||
const image = container.querySelector('img');
|
||||
|
||||
image.style.maxWidth = container.clientWidth + 'px';
|
||||
image.style.left = container.offsetLeft + container.clientWidth / 2 + 'px';
|
||||
};
|
||||
|
||||
new Photos(
|
||||
images.keys().map(k => imagePath(k)),
|
||||
document.querySelector('#landscapes'),
|
||||
document.querySelector('#portraits'),
|
||||
document.querySelector('#frame-image')
|
||||
);
|
||||
|
||||
addSupportForTabNavigation();
|
||||
window.addEventListener('resize', sizeFrame);
|
||||
sizeFrame();
|
||||
12
src/model/responsive-image.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type ResponsiveImage = {
|
||||
srcSet: string;
|
||||
src: string;
|
||||
placeholder: string;
|
||||
width: number;
|
||||
height: number;
|
||||
images: Array<{
|
||||
path: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
};
|
||||
109
src/photos.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { ResponsiveImage } from './model/responsive-image';
|
||||
import { createImage } from './helper/create-image';
|
||||
|
||||
export class Photos {
|
||||
private readonly images: Array<HTMLImageElement>;
|
||||
private _selectedId: number;
|
||||
private timerId: NodeJS.Timeout = null;
|
||||
private shouldNotChange = false;
|
||||
|
||||
public constructor(
|
||||
images: Array<ResponsiveImage>,
|
||||
private readonly landscapesContainer: HTMLElement,
|
||||
private readonly portraitsContainer: HTMLElement,
|
||||
private readonly pictureFrame: HTMLImageElement
|
||||
) {
|
||||
this.images = this.convertImages(images);
|
||||
this.placeImages();
|
||||
this.addEventListeners();
|
||||
this.startAutoChange();
|
||||
this.selectedId = 0;
|
||||
}
|
||||
|
||||
private get selectedId(): number {
|
||||
return this._selectedId;
|
||||
}
|
||||
|
||||
private set selectedId(value: number) {
|
||||
if (value < 0) {
|
||||
value += this.images.length - 1;
|
||||
}
|
||||
this._selectedId = value % this.images.length;
|
||||
this.loadToFrame(this.images[this.selectedId]);
|
||||
this.images[this.selectedId].focus();
|
||||
this.shouldNotChange = true;
|
||||
}
|
||||
|
||||
private convertImages(images: Array<ResponsiveImage>) {
|
||||
let portraits = 0; // evens
|
||||
let landscapes = 1; // odds
|
||||
|
||||
const imgs = images.map((image, i) =>
|
||||
createImage(
|
||||
image,
|
||||
'a photo',
|
||||
Photos.isLandscape(image) ? (landscapes += 2) : (portraits += 2)
|
||||
)
|
||||
);
|
||||
|
||||
return imgs.sort((a, b) => a.tabIndex - b.tabIndex);
|
||||
}
|
||||
|
||||
private placeImages() {
|
||||
this.images.forEach(img => {
|
||||
if (Photos.isLandscape(img)) {
|
||||
img.classList.add('landscape');
|
||||
this.landscapesContainer.appendChild(img);
|
||||
} else {
|
||||
img.classList.add('portrait');
|
||||
this.portraitsContainer.appendChild(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addEventListeners() {
|
||||
window.document.addEventListener('keydown', e => {
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
this.selectedId--;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.selectedId++;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (this.timerId !== null) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.document.addEventListener(
|
||||
'scroll',
|
||||
() => (this.shouldNotChange = true)
|
||||
);
|
||||
|
||||
this.images.forEach((image, i) =>
|
||||
image.addEventListener('click', () => (this.selectedId = i))
|
||||
);
|
||||
}
|
||||
|
||||
private startAutoChange() {
|
||||
this.timerId = setInterval(() => {
|
||||
if (!this.shouldNotChange) {
|
||||
this.selectedId++;
|
||||
}
|
||||
this.shouldNotChange = false;
|
||||
}, 7000);
|
||||
}
|
||||
|
||||
private loadToFrame(img: HTMLImageElement) {
|
||||
this.pictureFrame.src = img.src;
|
||||
}
|
||||
|
||||
private static isLandscape(
|
||||
image: HTMLImageElement | ResponsiveImage
|
||||
): boolean {
|
||||
return image.width > image.height;
|
||||
}
|
||||
}
|
||||
BIN
src/pictures/_DSC1190-Edit.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/pictures/_DSC1484.jpg
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
src/pictures/_DSC1834.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
src/pictures/_DSC2313.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/pictures/_DSC2362.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/pictures/_DSC2674.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/pictures/_DSC2692.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/pictures/_DSC8718-Edit.jpg
Normal file
|
After Width: | Height: | Size: 9.6 MiB |
BIN
src/pictures/adam.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/pictures/dorka-1.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/pictures/kezdetazarovonalbol.jpg
Normal file
|
After Width: | Height: | Size: 6.7 MiB |
BIN
src/pictures/nap-2.jpg
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
src/pictures/nyomtatás-10.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
src/pictures/nyomtatás-8.jpg
Normal file
|
After Width: | Height: | Size: 7.6 MiB |
BIN
src/pictures/web-1-2.jpg
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
src/pictures/web-1.jpg
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
src/pictures/web-10.jpg
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
src/pictures/web-11.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
src/pictures/web-12.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
src/pictures/web-13.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
src/pictures/web-14.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
src/pictures/web-17.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
src/pictures/web-18.jpg
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
src/pictures/web-19.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
src/pictures/web-2-2.jpg
Normal file
|
After Width: | Height: | Size: 413 KiB |
BIN
src/pictures/web-2.jpg
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
src/pictures/web-20.jpg
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
src/pictures/web-22.jpg
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
src/pictures/web-23.jpg
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
src/pictures/web-24.jpg
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
src/pictures/web-25.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
src/pictures/web-26.jpg
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
src/pictures/web-3-2.jpg
Normal file
|
After Width: | Height: | Size: 313 KiB |
BIN
src/pictures/web-3.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
src/pictures/web-4-2.jpg
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
src/pictures/web-42.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/pictures/web-5.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
src/pictures/web-6.jpg
Normal file
|
After Width: | Height: | Size: 465 KiB |
BIN
src/pictures/web-7.jpg
Normal file
|
After Width: | Height: | Size: 670 KiB |
BIN
src/pictures/web-8.jpg
Normal file
|
After Width: | Height: | Size: 810 KiB |