Update features
This commit is contained in:
parent
f66f052d7e
commit
4d7d15c3c7
34 changed files with 482 additions and 238 deletions
|
|
@ -1,9 +1,5 @@
|
|||
import { polyfillImul } from './polyfill-imul';
|
||||
|
||||
export class Random {
|
||||
public constructor(private seed: number) {
|
||||
polyfillImul();
|
||||
}
|
||||
public constructor(private seed: number) {}
|
||||
|
||||
public get next(): number {
|
||||
// result is in [0, 1)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Header } from '../../types/portfolio';
|
||||
|
||||
import './about.scss';
|
||||
import { Header } from '../../types/portfolio';
|
||||
import { html } from '../../types/html';
|
||||
|
||||
export const generate = ({ name }: Header): html => `
|
||||
<section id="about">
|
||||
<div class="picture"></div>
|
||||
<div class="placeholder"></div>
|
||||
<h1>${name}</h1>
|
||||
</section>
|
||||
<section id="about">
|
||||
<div class="picture"></div>
|
||||
<div class="placeholder"></div>
|
||||
<h1>${name}</h1>
|
||||
</section>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
section#about {
|
||||
@include card-base();
|
||||
padding: var(--normal-margin);
|
||||
background-color: var(--accent-color);
|
||||
font-size: 0;
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ section#about {
|
|||
p,
|
||||
h1 {
|
||||
color: var(--very-light-text-color);
|
||||
margin-top: var(--small-margin);
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
|
||||
@include on-large-screen {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { PageContent } from '../content/content';
|
||||
import { Header } from '../../types/portfolio';
|
||||
|
||||
import { generate } from './about.html';
|
||||
import { createElement } from '../../helper/create-element';
|
||||
import { PageThemeSwitcher } from '../theme-switcher/theme-switcher';
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ import './background.scss';
|
|||
import { html } from '../../types/html';
|
||||
|
||||
export const generate = (): html => `
|
||||
<canvas id="background"></canvas>
|
||||
<canvas id="background"></canvas>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import './anchor.scss';
|
||||
import { html } from '../../../types/html';
|
||||
import { url } from '../../../types/url';
|
||||
|
||||
export const generate = ({ href, text }: { href: string; text: string }): html => `
|
||||
export const generate = ({ href, text }: { href: url; text: string }): html => `
|
||||
<a class="primitive-anchor"
|
||||
href="${href}"
|
||||
target="_blank"
|
||||
|
|
|
|||
23
src/page/basics/image-anchor/image-anchor.html.ts
Normal file
23
src/page/basics/image-anchor/image-anchor.html.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import './image-anchor.scss';
|
||||
import { html } from '../../../types/html';
|
||||
import { url } from '../../../types/url';
|
||||
|
||||
export const generate = ({
|
||||
href,
|
||||
svg,
|
||||
title,
|
||||
}: {
|
||||
href: url;
|
||||
svg: url;
|
||||
title: string;
|
||||
}): html => `
|
||||
<a class="image-anchor"
|
||||
href="${href}"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="svgContainer">
|
||||
${svg}
|
||||
</div>
|
||||
<p>${title}</p>
|
||||
</a>
|
||||
`;
|
||||
21
src/page/basics/image-anchor/image-anchor.scss
Normal file
21
src/page/basics/image-anchor/image-anchor.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@use '../../../style/mixins' as *;
|
||||
|
||||
.image-anchor {
|
||||
@include image-button(var(--icon-size));
|
||||
|
||||
.svgContainer {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
@include square(var(--icon-size));
|
||||
|
||||
svg {
|
||||
stroke: var(--normal-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
padding-bottom: var(--small-margin);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
11
src/page/basics/image-anchor/image-anchor.ts
Normal file
11
src/page/basics/image-anchor/image-anchor.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { PageElement } from '../../page-element';
|
||||
import { createElement } from '../../../helper/create-element';
|
||||
import { url } from '../../../types/url';
|
||||
import { generate } from './image-anchor.html';
|
||||
|
||||
export const ImageAnchorFactory = (svg: string, title: string) =>
|
||||
class ImageAnchor extends PageElement {
|
||||
public constructor(href: url) {
|
||||
super(createElement(generate({ href, svg, title })));
|
||||
}
|
||||
};
|
||||
|
|
@ -16,10 +16,10 @@ export const generate = ({
|
|||
}): html => `
|
||||
${container ? `<div class="figure-container">` : ''}
|
||||
<img tabindex="0"
|
||||
srcset="${image.srcSet}"
|
||||
sizes="${sizes}"
|
||||
src="${last(image.images)?.path}"
|
||||
alt="${alt}"
|
||||
srcset="${image.srcSet}"
|
||||
sizes="${sizes}"
|
||||
src="${last(image.images)?.path}"
|
||||
alt="${alt}"
|
||||
/>
|
||||
${container ? `</div>` : ''}
|
||||
`;
|
||||
|
|
|
|||
15
src/page/basics/preview/preview.html.ts
Normal file
15
src/page/basics/preview/preview.html.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import './preview.scss';
|
||||
import play from '../../../static/icons/play-button.svg';
|
||||
import loading from '../../../static/icons/loading.svg';
|
||||
import { html } from '../../../types/html';
|
||||
|
||||
export const generate = ({ alt }: { alt: string }): html => `
|
||||
<div class="preview">
|
||||
<img image-viewer-ignore class="poster" />
|
||||
<div class="overlay">
|
||||
<iframe title="${alt}" height=300 allowfullscreen loading="lazy"></iframe>
|
||||
<div class="loading">${loading}</div>
|
||||
<div class="load-button">${play}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
65
src/page/basics/preview/preview.scss
Normal file
65
src/page/basics/preview/preview.scss
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
@use '../../../style/mixins' as *;
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
|
||||
.overlay {
|
||||
@include square(100%);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
* {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.load-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: 10000px;
|
||||
backdrop-filter: blur(16px);
|
||||
transition: transform var(--transition-time), box-shadow var(--transition-time);
|
||||
stroke: var(--normal-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
visibility: hidden;
|
||||
&,
|
||||
& > svg {
|
||||
@include square(var(--large-icon-size));
|
||||
@include absolute-center;
|
||||
}
|
||||
}
|
||||
|
||||
iframe {
|
||||
@include square(100%);
|
||||
border: none;
|
||||
&:fullscreen {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.loaded {
|
||||
.figure-container,
|
||||
.load-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
.loading {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/page/basics/preview/preview.ts
Normal file
39
src/page/basics/preview/preview.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { PageElement } from '../../page-element';
|
||||
import { createElement } from '../../../helper/create-element';
|
||||
import { generate } from './preview.html';
|
||||
import { Image } from '../image/image';
|
||||
import { ResponsiveImage } from '../../../types/responsive-image';
|
||||
import { OnLoadEvent } from '../../../events/concrete-events/on-load-event';
|
||||
|
||||
export class Preview extends PageElement {
|
||||
public constructor(poster: ResponsiveImage, private readonly url: string, alt: string) {
|
||||
super(createElement(generate({ alt })));
|
||||
this.url += '?portfolioView';
|
||||
this.attachElementByReplacing('.poster', new Image(poster, alt));
|
||||
this.query('.load-button').addEventListener('click', this.loadContent.bind(this));
|
||||
this.query('iframe').addEventListener('load', () => {
|
||||
this.htmlRoot.classList.remove('waiting');
|
||||
});
|
||||
}
|
||||
|
||||
public handleOnLoadEvent(event: OnLoadEvent): OnLoadEvent {
|
||||
new IntersectionObserver(e => {
|
||||
if (!e[0].isIntersecting) {
|
||||
this.unloadContent();
|
||||
}
|
||||
}).observe(this.htmlRoot.parentElement);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
public loadContent() {
|
||||
this.htmlRoot.classList.add('waiting');
|
||||
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,4 +1,3 @@
|
|||
.primitive-text {
|
||||
text-align: left;
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ footer#page-footer {
|
|||
margin-top: var(--large-margin);
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
@include link;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include title-font();
|
||||
}
|
||||
|
|
@ -27,11 +31,8 @@ footer#page-footer {
|
|||
img,
|
||||
svg {
|
||||
@include max-square(var(--icon-size));
|
||||
margin-right: var(--small-margin);
|
||||
|
||||
* {
|
||||
fill: var(--normal-text-color);
|
||||
}
|
||||
margin-right: calc(var(--small-margin) / 2);
|
||||
stroke: var(--normal-text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import './image-viewer.scss';
|
|||
import { html } from '../../types/html';
|
||||
|
||||
export const generate = (): html => `
|
||||
<section id="image-viewer">
|
||||
<div id="container"></div>
|
||||
<img tabindex="0" id="cancel" src="${cancel}" alt="cancel"/>
|
||||
</section>
|
||||
<section id="image-viewer">
|
||||
<div id="container"></div>
|
||||
<div tabindex="0" id="cancel">${cancel}</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -28,12 +28,11 @@ section#image-viewer {
|
|||
}
|
||||
|
||||
#cancel {
|
||||
@include square(var(--icon-size));
|
||||
@include square(calc(var(--large-icon-size) + var(--normal-margin) * 2));
|
||||
position: absolute;
|
||||
box-sizing: content-box;
|
||||
padding: var(--normal-margin);
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@include image-button(var(--large-icon-size));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ export class PageImageViewer extends PageElement {
|
|||
const media = Array.prototype.slice.call(document.querySelectorAll('img'));
|
||||
|
||||
media
|
||||
.filter((e: HTMLElement) => e.parentElement !== this.htmlRoot)
|
||||
.filter((e: HTMLElement) => !e.attributes['image-viewer-ignore'])
|
||||
.forEach((e: HTMLImageElement) => (e.onclick = this.handleClick.bind(this)));
|
||||
|
||||
return super.handleOnLoadEvent(event);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ export abstract class PageElement extends EventHandler implements EventBroadcast
|
|||
this.children.push(element);
|
||||
}
|
||||
|
||||
protected attachElementAsChildOf(query: string, element: PageElement) {
|
||||
this.query(query).appendChild(element.htmlRoot);
|
||||
this.children.push(element);
|
||||
}
|
||||
|
||||
protected attachElement(element: PageElement) {
|
||||
this.htmlRoot.appendChild(element.htmlRoot);
|
||||
this.children.push(element);
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ import './theme-switcher.scss';
|
|||
import { html } from '../../types/html';
|
||||
|
||||
export const generate = (): html => `
|
||||
<input id="theme-switcher" aria-label="color-theme-switch" type="checkbox" name="switch-theme"/>
|
||||
<input id="theme-switcher" aria-label="color-theme-switch" type="checkbox" name="switch-theme"/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,35 @@
|
|||
import { TimelineElement } from '../../../types/portfolio';
|
||||
|
||||
import info from '../../../static/icons/info.svg';
|
||||
import './timeline-element.scss';
|
||||
import { html } from '../../../types/html';
|
||||
|
||||
export const generate = (
|
||||
{ date, title, more }: TimelineElement,
|
||||
showMore: string,
|
||||
showLess: string
|
||||
showMore: string
|
||||
): html => `
|
||||
<section class="timeline-element">
|
||||
<div class="line-container">
|
||||
<div class="line"></div>
|
||||
<p class="date">${date}</p>
|
||||
<section class="timeline-element">
|
||||
<div class="line-container">
|
||||
<div class="line"></div>
|
||||
<p class="date">${date}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="figure"></div>
|
||||
<div class="lower">
|
||||
<h2>${title}</h2>
|
||||
<div class="description"></div>
|
||||
${more ? '<div class="more"></div>' : ''}
|
||||
<div class="buttons">
|
||||
${
|
||||
more
|
||||
? `
|
||||
<div class="info-button">
|
||||
<div class="svgContainer">${info}</div>
|
||||
<p>${showMore}</p>
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>${title}</h2>
|
||||
<div class="figure"></div>
|
||||
<div class="description"></div>
|
||||
${
|
||||
more
|
||||
? `
|
||||
<div class="more"></div>
|
||||
<div class="buttons">
|
||||
<a tabindex="0" class="show-more">${showMore}</a>
|
||||
<a tabindex="0" class="show-less">${showLess}</a>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
<div class="link"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -91,46 +91,75 @@ section.timeline-element {
|
|||
background-color: var(--card-color);
|
||||
}
|
||||
|
||||
& > *:not(:first-child) {
|
||||
margin-top: var(--line-height);
|
||||
img,
|
||||
video,
|
||||
iframe {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0;
|
||||
}
|
||||
$border-width: 1px;
|
||||
|
||||
h2 {
|
||||
@include sub-title-font();
|
||||
}
|
||||
|
||||
& > p {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
transition: height var(--transition-time);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: relative;
|
||||
margin-top: var(--line-height);
|
||||
|
||||
.show-more,
|
||||
.show-less {
|
||||
transition: opacity var(--transition-time);
|
||||
.lower {
|
||||
& > * {
|
||||
padding: 0 var(--normal-margin);
|
||||
margin-top: var(--small-margin);
|
||||
}
|
||||
|
||||
.show-more {
|
||||
opacity: 1;
|
||||
h2 {
|
||||
@include sub-title-font();
|
||||
}
|
||||
|
||||
.show-less {
|
||||
@include absolute-center();
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
& > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
height: 0;
|
||||
transition: height var(--transition-time);
|
||||
|
||||
.content p {
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: $border-width solid var(--normal-text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: var(--small-margin);
|
||||
|
||||
.info-button {
|
||||
@include image-button(var(--icon-size));
|
||||
|
||||
.svgContainer {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
@include square(var(--icon-size));
|
||||
|
||||
svg {
|
||||
stroke: var(--normal-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
padding-bottom: var(--small-margin);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
|
||||
padding-top: var(--small-margin);
|
||||
&:not(:last-child) {
|
||||
border-right: $border-width solid var(--normal-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import { OnBodyDimensionsChangedEvent } from '../../../events/concrete-events/on
|
|||
export class PageTimelineElement extends PageElement {
|
||||
private isOpen: boolean;
|
||||
private more: HTMLElement;
|
||||
private showMore: string;
|
||||
private showLess: string;
|
||||
|
||||
public constructor(
|
||||
timelineElement: TimelineElement,
|
||||
showMore: string,
|
||||
showLess: string
|
||||
) {
|
||||
const root = createElement(generate(timelineElement, showMore, showLess));
|
||||
const root = createElement(generate(timelineElement, showMore));
|
||||
|
||||
if (timelineElement.more) {
|
||||
const content = new PageContent(timelineElement.more);
|
||||
|
|
@ -23,30 +25,25 @@ export class PageTimelineElement extends PageElement {
|
|||
this.isOpen = false;
|
||||
this.more = root.querySelector('.more');
|
||||
this.more.appendChild(content.htmlRoot);
|
||||
window.addEventListener('resize', this.handleResize.bind(this));
|
||||
root
|
||||
.querySelector('.buttons')
|
||||
.addEventListener('click', this.toggleOpen.bind(this));
|
||||
addEventListener('resize', this.handleResize.bind(this));
|
||||
|
||||
this.query('.info-button').addEventListener('click', this.toggleOpen.bind(this));
|
||||
} else super(root);
|
||||
|
||||
this.attachElementByReplacing('.figure', timelineElement.figure);
|
||||
this.attachElementByReplacing('.description', timelineElement.description);
|
||||
timelineElement.links.forEach(l => this.attachElementAsChildOf('.buttons', l));
|
||||
|
||||
if (timelineElement.link) {
|
||||
this.attachElementByReplacing('.link', timelineElement.link);
|
||||
}
|
||||
this.showMore = showMore;
|
||||
this.showLess = showLess;
|
||||
}
|
||||
|
||||
private toggleOpen() {
|
||||
const showMore = this.query('.show-more') as HTMLElement;
|
||||
const showLess = this.query('.show-less') as HTMLElement;
|
||||
if (this.isOpen) {
|
||||
PageTimelineElement.show(showMore);
|
||||
PageTimelineElement.hide(showLess);
|
||||
this.query('.info-button p').innerText = this.showMore;
|
||||
this.closeMore();
|
||||
} else {
|
||||
PageTimelineElement.show(showLess);
|
||||
PageTimelineElement.hide(showMore);
|
||||
this.query('.info-button p').innerText = this.showLess;
|
||||
this.openMore();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ import './timeline.scss';
|
|||
import { html } from '../../types/html';
|
||||
|
||||
export const generate = (): html => `
|
||||
<div id="timeline"></div>
|
||||
<div id="timeline"></div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
@use '../../style/mixins' as *;
|
||||
|
||||
div#timeline {
|
||||
@include on-large-screen {
|
||||
// workaround for IE
|
||||
& > :first-child {
|
||||
margin-top: var(--large-margin);
|
||||
}
|
||||
@include on-large-screen {
|
||||
div#timeline > :first-child {
|
||||
margin-top: var(--large-margin);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
139
src/portfolio.ts
139
src/portfolio.ts
|
|
@ -6,10 +6,18 @@ import { PageHeader } from './page/about/about';
|
|||
import { PageTimeline } from './page/timeline/timeline';
|
||||
import { PageImageViewer } from './page/image-viewer/image-viewer';
|
||||
import { last } from './helper/last';
|
||||
import { PageBackground } from './page/background/background';
|
||||
import { Anchor } from './page/basics/anchor/anchor';
|
||||
import { Body } from './page/body/body';
|
||||
import { ImageAnchorFactory } from './page/basics/image-anchor/image-anchor';
|
||||
import { Preview } from './page/basics/preview/preview';
|
||||
import me from './static/media/me.jpg';
|
||||
import declared from './static/media/decla-red.png';
|
||||
import forexMP4 from './static/media/forex.mp4';
|
||||
import forexWEBM from './static/media/forex.webm';
|
||||
import thesis from './static/media/andras-schmelczer-thesis.pdf';
|
||||
import adAstraMP4 from './static/media/ad_astra_720.mp4';
|
||||
import cvIcon from './static/icons/cv.svg';
|
||||
import adAstraWEBM from './static/media/ad_astra_720.webm';
|
||||
import ad_astra_index from './static/media/ad_astra.jpg';
|
||||
import myNotes from './static/media/my-notes.png';
|
||||
|
|
@ -24,27 +32,26 @@ import led from './static/media/led.jpg';
|
|||
import cvEnglish from './static/cv/cv_andras_schmelczer.pdf';
|
||||
import ledMP4 from './static/media/led.mp4';
|
||||
import ledWEBM from './static/media/led.webm';
|
||||
import { PageBackground } from './page/background/background';
|
||||
import { Anchor } from './page/basics/anchor/anchor';
|
||||
|
||||
import { Body } from './page/body/body';
|
||||
import githubIcon from './static/icons/github.svg';
|
||||
import openIcon from './static/icons/open.svg';
|
||||
|
||||
export const create = () => {
|
||||
const GitHub = ImageAnchorFactory(githubIcon, 'Open on GitHub');
|
||||
const Open = ImageAnchorFactory(openIcon, 'Open in new tab');
|
||||
const Thesis = ImageAnchorFactory(cvIcon, 'Download thesis');
|
||||
|
||||
const page = {
|
||||
imageViewer: new PageImageViewer(),
|
||||
header: new PageHeader({
|
||||
name: `András Schmelczer`,
|
||||
picture: new Image(me, `a picture of me`, false),
|
||||
about: [
|
||||
new Text(
|
||||
`I have always been fascinated by the engineering feats that surround us.
|
||||
When I realized that someday I might be able to contribute to these achievements,
|
||||
I knew that is what I need to aim for. As I am starting my last semester at the
|
||||
Budapest University of Technology and Economics, I feel I am getting closer to it every day.`
|
||||
),
|
||||
new Text(
|
||||
`You can see some of the more interesting projects I have worked on below.`
|
||||
),
|
||||
new Text(`I have always been fascinated by the engineering feats that surround us and pervade every aspect
|
||||
of our lives. When I realised I might someday be able to contribute to this field, I knew that
|
||||
this would become my life’s ambition. As I am finishing my last semester at the
|
||||
Budapest University of Technology and Economics, I feel I am getting closer to it every day.`),
|
||||
new Text(`Look at some of the more interesting projects I have worked on. They are all listed below.
|
||||
Further information about me can be found at the bottom of the page.`),
|
||||
],
|
||||
}),
|
||||
timeline: new PageTimeline({
|
||||
|
|
@ -52,29 +59,53 @@ export const create = () => {
|
|||
showLessText: `Show less`,
|
||||
elements: [
|
||||
{
|
||||
title: `SDF-2D library`,
|
||||
title: `Multiplayer game`,
|
||||
date: `2020 Autumn`,
|
||||
figure: new Image(sdf2d, `a screenshot of a demo scene`),
|
||||
figure: new Preview(
|
||||
declared,
|
||||
'https://decla.red',
|
||||
'The website of the video game'
|
||||
),
|
||||
description: new Text(
|
||||
`I created an NPM package for efficiently rendering and shading 2D scenes described
|
||||
by signed distance fields (SDF-s). It supports both WebGL and WebGL2 and is easily extendible.`
|
||||
`Using SDF-2D, I developed a conquest-style multiplayer browser game. It even runs on mobiles.`
|
||||
),
|
||||
more: [
|
||||
new Text(
|
||||
`A multitude of optimisations were needed to achieve real-time performance even on low-end mobile devices.
|
||||
These include deferred shading, tile-based rendering, and dynamic shader generation to minimize unnecessary
|
||||
instructions. Additionally, there were some interesting quirks of specific hardware that also needed to be overcame.`
|
||||
),
|
||||
new Text(
|
||||
`The end result is a reusable library written in TypeScript with a simple and elegant API.
|
||||
new Text(`The scene is set in space, two teams have to conquer small planets, while they can also shoot at the other team.
|
||||
Points are given based on the number of planets controlled,
|
||||
and the first team which reaches a predefined score wins.`),
|
||||
new Text(`As for the communication, a server-client architecture is used. Messaging is provided by Socket.IO and a custom
|
||||
serialisation solution.`),
|
||||
new Text(`This (along with SDF-2D) was my BSc thesis project, so more in-depth information about them
|
||||
can be found in my thesis linked below.`),
|
||||
],
|
||||
links: [
|
||||
new GitHub('https://github.com/schmelczerandras/decla.red'),
|
||||
new Thesis(thesis),
|
||||
new Open('https://decla.red'),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: `2D ray tracing`,
|
||||
date: `2020 Autumn`,
|
||||
figure: new Preview(
|
||||
sdf2d,
|
||||
'https://sdf2d.schmelczer.dev',
|
||||
'A webpage showcasing the SDF-2D project.'
|
||||
),
|
||||
description: new Text(`I created the SDF-2D library for efficiently rendering 2D scenes using ray tracing.
|
||||
My solution relies on signed distance fields (SDF-s), it supports both WebGL and WebGL2,
|
||||
and is an easily reusable and extendible NPM package.`),
|
||||
more: [
|
||||
new Text(`A multitude of optimisations were needed to achieve real-time performance even on low-end mobile devices.
|
||||
These include deferred shading, tile-based rendering, and dynamic shader generation to eliminate unnecessary
|
||||
instructions. Additionally, there were some interesting quirks of specific hardware that also needed to be overcome.`),
|
||||
new Text(`The end result is a reusable library written in TypeScript with a — subjectively — simple and elegant API.
|
||||
For more information please check out the GitHub repository or the NPM package itself. Or simply enjoy the
|
||||
mesmerizing demo scenes.`
|
||||
),
|
||||
new Anchor(`https://sdf2d.schmelczer.dev`, `View it in action`),
|
||||
new Anchor(
|
||||
`https://github.com/schmelczerandras/sdf-2d`,
|
||||
`Check it out on GitHub`
|
||||
),
|
||||
mesmerizing demo scenes.`),
|
||||
],
|
||||
links: [
|
||||
new GitHub('https://github.com/schmelczerandras/sdf-2d'),
|
||||
new Open('https://sdf2d.schmelczer.dev'),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -86,33 +117,24 @@ export const create = () => {
|
|||
adAstraWEBM,
|
||||
`controls playsinline preload="none"`
|
||||
),
|
||||
description: new Text(
|
||||
`A simple game engine with a sample game set in space. The greatest challenge was to overcome
|
||||
the very limited resources of the hardware, this was also the most rewarding part.`
|
||||
),
|
||||
description: new Text(`A simple game engine with a sample game set in space. The greatest challenge was to overcome
|
||||
the very limited resources of the hardware, this was also the most rewarding part.`),
|
||||
more: [
|
||||
new Text(
|
||||
`For reducing complexity while maintaining performance a balance had to be found between object-oriented
|
||||
new Text(`For reducing complexity while maintaining performance, a balance had to be found between object-oriented
|
||||
and structural programming. For example, a simple prototype-based inheritance is used for the game objects.
|
||||
An optimized SIMD utilizing low level driver is used for drawing on the display.
|
||||
I think the code base is quite readable and at the same time the
|
||||
maximum frame times are between 15ms and 20ms at 8MHz.`
|
||||
),
|
||||
new Text(
|
||||
`As for the hardware, it is rather simple. Aside from the ATtiny85V, a D096-12864-SPI7 display is used for
|
||||
output and a TSOP4838 for input. The circuit runs on 3.3V, so a regulator is also needed. It uses a current
|
||||
of 8mA to 11mA on full brightness and around 1.5mA on standby mode.`
|
||||
),
|
||||
Meanwhile, an optimized SIMD utilizing low-level driver is used for drawing on the display.
|
||||
I think the codebase is quite readable and at the same time the
|
||||
maximum frame times are between 15 ms and 20 ms at 8 MHz clock speed, which I find quite impressive.`),
|
||||
new Text(`As for the hardware, it is rather simple. Aside from the ATtiny85V, a D096-12864-SPI7 display is used for
|
||||
output and a TSOP4838 for input. The circuit runs on 3.3V, so a regulator is also needed. It uses a current
|
||||
of 8mA to 11mA on full brightness and around 1.5mA on standby mode.`),
|
||||
new Text(
|
||||
`There is also fault-tolerant persistent data storage using the built-in EEPROM.
|
||||
For creating sprites (which are also stored in EEPROM) I made a tool to convert PNG-s into C code.
|
||||
This can also be found on GitHub as well as the entire project.`
|
||||
),
|
||||
new Anchor(
|
||||
`https://github.com/schmelczerandras/ad_astra`,
|
||||
`View it on GitHub`
|
||||
For creating sprites (which are also stored in EEPROM) I made a tool to convert PNG-s into C code.
|
||||
This can also be found on GitHub as well as the entire project.`
|
||||
),
|
||||
],
|
||||
links: [new GitHub('https://github.com/schmelczerandras/ad_astra')],
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -140,6 +162,7 @@ export const create = () => {
|
|||
a mostly profitable trading strategy is viable. In my free time I may put more work into it.`
|
||||
),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
date: `2019 November`,
|
||||
|
|
@ -161,6 +184,7 @@ export const create = () => {
|
|||
It was also my first experience with Android development.`
|
||||
),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
date: `2018 October - November`,
|
||||
|
|
@ -183,6 +207,7 @@ export const create = () => {
|
|||
the communication between the layers. For drawing the frontend HTML5 canvas is utilized.`
|
||||
),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
date: `2018 October - November`,
|
||||
|
|
@ -201,6 +226,7 @@ export const create = () => {
|
|||
directly uploaded to the simulation backend.`
|
||||
),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
date: `2018 July - August`,
|
||||
|
|
@ -229,6 +255,7 @@ export const create = () => {
|
|||
were also made by me using Blender.`
|
||||
),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
date: `2018 June`,
|
||||
|
|
@ -251,8 +278,8 @@ export const create = () => {
|
|||
`By clicking on a coloured circle you can change its settings.
|
||||
New circles can be created by clicking in the large circle (and they can also be moved by drag & drop).`
|
||||
),
|
||||
new Anchor('https://color.schmelczer.dev', `color.schmelczer.dev`),
|
||||
],
|
||||
links: [new Open('color.schmelczer.dev')],
|
||||
},
|
||||
{
|
||||
date: `2017 autumn`,
|
||||
|
|
@ -268,13 +295,14 @@ export const create = () => {
|
|||
),
|
||||
new Text(`I did this as a homework for my Basics of Programming course.`),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
date: `2016 summer`,
|
||||
title: `Photos`,
|
||||
figure: new Image(photos, `a picture of the website`),
|
||||
description: new Text(`A simple web page where you can view my photos.`),
|
||||
link: new Anchor(`https://photo.schmelczer.dev`, `photo.schmelczer.dev`),
|
||||
links: [new Open('https://photo.schmelczer.dev')],
|
||||
},
|
||||
{
|
||||
date: `2016 spring`,
|
||||
|
|
@ -300,12 +328,13 @@ export const create = () => {
|
|||
the settings also got built using vanilla web development technologies.`
|
||||
),
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
footer: new PageFooter({
|
||||
title: `Learn more`,
|
||||
curiumVitaes: [{ name: `Curriculum vitae`, url: cvEnglish }],
|
||||
curriculaVitae: [{ name: `Curriculum vitae`, url: cvEnglish }],
|
||||
email: `andras@schmelczer.dev`,
|
||||
lastEditText: `Last modified on `,
|
||||
lastEdit: new Date(2020, 11 - 1, 17), // months are 0 indexed
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 241.171 241.171" style="enable-background:new 0 0 241.171 241.171;" xml:space="preserve" width="512px" height="512px">
|
||||
<path d="M138.138,120.754l99.118-98.576c4.752-4.704,4.752-12.319,0-17.011c-4.74-4.704-12.439-4.704-17.179,0 l-99.033,98.492L21.095,3.699c-4.74-4.752-12.439-4.752-17.179,0c-4.74,4.764-4.74,12.475,0,17.227l99.876,99.888L3.555,220.497 c-4.74,4.704-4.74,12.319,0,17.011c4.74,4.704,12.439,4.704,17.179,0l100.152-99.599l99.551,99.563 c4.74,4.752,12.439,4.752,17.179,0c4.74-4.764,4.74-12.475,0-17.227L138.138,120.754z" class="active-path" fill="#fff9e0"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 730 B After Width: | Height: | Size: 321 B |
BIN
src/static/media/andras-schmelczer-thesis.pdf
Normal file
BIN
src/static/media/andras-schmelczer-thesis.pdf
Normal file
Binary file not shown.
BIN
src/static/media/decla-red.png
Normal file
BIN
src/static/media/decla-red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -1,48 +0,0 @@
|
|||
@use 'sass:color';
|
||||
@use 'style/mixins' as *;
|
||||
|
||||
a {
|
||||
$border-shift: 10px;
|
||||
$line-width: 2px;
|
||||
|
||||
@include special-text-font();
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
|
||||
padding: 0 3px $line-width 0;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
width: calc(100% + #{$border-shift});
|
||||
border-bottom: $line-width dashed var(--accent-color);
|
||||
transition: transform var(--transition-time);
|
||||
}
|
||||
|
||||
&:after {
|
||||
width: 100%;
|
||||
height: $line-width;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--card-color) 0,
|
||||
transparent 4px,
|
||||
transparent calc(100% - 4px),
|
||||
var(--card-color) 100%
|
||||
);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:before {
|
||||
transform: translateX(-$border-shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,24 @@ $breakpoint-width: 925px !default;
|
|||
}
|
||||
}
|
||||
|
||||
@mixin image-button($icon-size) {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover svg {
|
||||
transform: translateX(-50%) translateY(-50%) scale(1.15);
|
||||
}
|
||||
|
||||
svg {
|
||||
@include absolute-center;
|
||||
@include square($icon-size);
|
||||
transition: transform var(--transition-time);
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin center-children() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -27,7 +45,6 @@ $breakpoint-width: 925px !default;
|
|||
|
||||
@mixin card-base() {
|
||||
text-align: center;
|
||||
padding: var(--normal-margin);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
|
|
@ -64,7 +81,7 @@ $breakpoint-width: 925px !default;
|
|||
@mixin main-font() {
|
||||
font: 400 1.1rem 'Open Sans', sans-serif;
|
||||
color: var(--normal-text-color);
|
||||
line-height: 1.6;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@mixin special-text-font() {
|
||||
|
|
@ -72,3 +89,49 @@ $breakpoint-width: 925px !default;
|
|||
color: var(--special-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@mixin link {
|
||||
$border-shift: 10px;
|
||||
$line-width: 2px;
|
||||
|
||||
@include special-text-font();
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
|
||||
padding: 0 3px $line-width 0;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
width: calc(100% + #{$border-shift});
|
||||
border-bottom: $line-width dashed var(--accent-color);
|
||||
transition: transform var(--transition-time);
|
||||
}
|
||||
|
||||
&:after {
|
||||
width: 100%;
|
||||
height: $line-width;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--card-color) 0,
|
||||
transparent 4px,
|
||||
transparent calc(100% - 4px),
|
||||
var(--card-color) 100%
|
||||
);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:before {
|
||||
transform: translateX(-$border-shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@
|
|||
--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 0 10px 2px rgba(0, 0, 0, 0.075),
|
||||
inset 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
--icon-size: 35px;
|
||||
--inset-shadow: inset 0 -9px 7px -7px rgb(0, 0, 0, 0.07);
|
||||
--icon-size: 45px;
|
||||
--large-icon-size: 80px;
|
||||
--body-width: 765px;
|
||||
}
|
||||
}
|
||||
|
|
@ -41,9 +41,9 @@
|
|||
--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 0 10px 2px rgba(0, 0, 0, 0.05),
|
||||
inset 0 0 1px rgba(0, 0, 0, 0.125);
|
||||
--icon-size: 25px;
|
||||
--inset-shadow: inset 0 -9px 7px -7px rgb(0, 0, 0, 0.07);
|
||||
--icon-size: 35px;
|
||||
--large-icon-size: 55px;
|
||||
--body-width: 90%;
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
--background: #242638;
|
||||
--normal-text-color: #ffffff;
|
||||
--card-color: #263551;
|
||||
--blurred-card-color: #26355166;
|
||||
--blurred-card-color: #26355155;
|
||||
--blur-radius: 24px;
|
||||
--special-text-color: #ffffff;
|
||||
--inset-shadow: inset 0 0 10px 2px rgba(0, 0, 0, 0.175),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
@use 'style/vars';
|
||||
@use 'style/mixins' as *;
|
||||
@use 'style/a';
|
||||
@use 'style/animations/animations';
|
||||
@use 'style/dark-mode/dark-mode';
|
||||
|
||||
|
|
@ -55,7 +54,7 @@ html {
|
|||
.figure-container {
|
||||
font-size: 0;
|
||||
box-shadow: var(--inset-shadow);
|
||||
margin-top: var(--line-height);
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Video } from '../page/basics/video/video';
|
||||
import { Text } from '../page/basics/text/text';
|
||||
import { Image } from '../page/basics/image/image';
|
||||
import { Anchor } from '../page/basics/anchor/anchor';
|
||||
|
||||
import { PageElement } from '../page/page-element';
|
||||
import { url } from './url';
|
||||
import { Preview } from '../page/basics/preview/preview';
|
||||
|
||||
export interface Portfolio {
|
||||
header: Header;
|
||||
|
|
@ -24,18 +25,18 @@ export interface Timeline {
|
|||
}
|
||||
|
||||
export interface TimelineElement {
|
||||
title: string;
|
||||
date: string;
|
||||
figure: Image | Video;
|
||||
figure: Image | Video | Preview;
|
||||
title: string;
|
||||
description: Text;
|
||||
more?: Content;
|
||||
link?: Anchor;
|
||||
links: Array<PageElement>;
|
||||
}
|
||||
|
||||
export interface Footer {
|
||||
title: string;
|
||||
email: string;
|
||||
curiumVitaes: Array<CV>;
|
||||
curriculaVitae: Array<CV>;
|
||||
lastEditText: string;
|
||||
lastEdit: Date;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue