Fix background and fix PageElement system

This commit is contained in:
schmelczerandras 2020-11-22 22:41:10 +01:00
parent 6fc16f4de0
commit 91d92f7f48
24 changed files with 528 additions and 809 deletions

View file

@ -1,9 +1,11 @@
{
"cSpell.words": [
"Glsl",
"andras",
"decla",
"favicons",
"forex",
"froms",
"schmelczer",
"webm",
"webp"

View file

@ -1,12 +0,0 @@
import { Event } from '../event';
import { EventHandler } from '../event-handler';
import { EventBroadcaster } from '../event-broadcaster';
import { OptionalEvent } from '../optional-event';
export class OnEventBroadcasterChangedEvent implements Event {
public constructor(public broadcaster: EventBroadcaster) {}
public accept(handler: EventHandler): OptionalEvent {
return handler.handleOnEventBroadcasterChangedEvent(this);
}
}

View file

@ -1,12 +0,0 @@
import { Event } from '../event';
import { EventHandler } from '../event-handler';
import { OptionalEvent } from '../optional-event';
import { PageElement } from '../../page/page-element';
export class OnLoadEvent implements Event {
public constructor(public parent: PageElement) {}
public accept(handler: EventHandler): OptionalEvent {
return handler.handleOnLoadEvent(this);
}
}

View file

@ -1,11 +0,0 @@
import { Event } from '../event';
import { EventHandler } from '../event-handler';
import { OptionalEvent } from '../optional-event';
export class OnPageThemeChangedEvent implements Event {
public constructor(public isDark: boolean) {}
public accept(handler: EventHandler): OptionalEvent {
return handler.handleOnPageThemeChangedEvent(this);
}
}

View file

@ -1,5 +0,0 @@
import { Event } from './event';
export interface EventBroadcaster {
broadcastEvent(event: Event): void;
}

View file

@ -1,26 +0,0 @@
import { Event } from './event';
import { OnLoadEvent } from './concrete-events/on-load-event';
import { OnPageThemeChangedEvent } from './concrete-events/on-page-theme-changed-event';
import { OnEventBroadcasterChangedEvent } from './concrete-events/on-event-broadcaster-changed-event';
import { OptionalEvent } from './optional-event';
export abstract class EventHandler {
public handle(event: Event): OptionalEvent {
return event.accept(this);
}
public handleOnLoadEvent(event: OnLoadEvent): OptionalEvent {
return event;
}
public handleOnEventBroadcasterChangedEvent(
event: OnEventBroadcasterChangedEvent
): OptionalEvent {
return event;
}
public handleOnPageThemeChangedEvent(event: OnPageThemeChangedEvent): OptionalEvent {
return event;
}
}

View file

@ -1,6 +0,0 @@
import { EventHandler } from './event-handler';
import { OptionalEvent } from './optional-event';
export interface Event {
accept(handler: EventHandler): OptionalEvent;
}

View file

@ -1,3 +0,0 @@
import { Event } from './event';
export type OptionalEvent = Event | undefined;

View file

@ -1,5 +1,5 @@
export class Random {
public constructor(private seed: number) {}
public constructor(public seed: number = 42) {}
public get next(): number {
// result is in [0, 1)
@ -7,10 +7,10 @@ export class Random {
}
public choose<T>(list: Array<T>): T {
return list[Math.floor(this.randomInInterval(0, list.length))];
return list[Math.floor(this.inInterval(0, list.length))];
}
public randomInInterval(aClosed: number, bOpen: number): number {
public inInterval(aClosed: number, bOpen: number): number {
return (bOpen - aClosed) * this.next + aClosed;
}
}

View file

@ -5,10 +5,10 @@
<meta property="og:image:width" content="1500" />
<meta property="og:image:height" content="785" />
<meta property="og:title" content="Andr&aacute;s Schmelczer - Portfolio" />
<meta property="og:image" content="https://schmelczer.dev/og-image.jpg" />
<meta property="og:title" content="Portfolio - Andr&aacute;s Schmelczer" />
<meta property="og:description" content="Discover my projects." />
<meta property="og:url" content="https://schmelczer.dev" />
<meta property="og:image" content="https://schmelczer.dev/og-image.jpg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" href="favicon.ico" type="image/x-icon" />
@ -30,11 +30,9 @@
rel="stylesheet"
/>
<title>András Schmelczer - Portfolio</title>
<title>Portfolio - András Schmelczer</title>
</head>
<body>
<main>
<noscript><h1>Javascript is required for this website.</h1></noscript>
</main>
<noscript>Javascript is required for this website.</noscript>
</body>
</html>

View file

@ -1,30 +0,0 @@
export class Animation<T> {
private _value: T;
private elapsedTime = 0;
public constructor(
private from: T,
private to: T,
private intervalInMs: number,
private interpolator: (from: T, to: T, q: number) => T,
private onChange?: (currentValue: T) => void
) {
this._value = from;
}
public step(deltaTimeInMs: number) {
if (this.elapsedTime === this.intervalInMs) {
return;
}
this.elapsedTime = Math.min(this.elapsedTime + deltaTimeInMs, this.intervalInMs);
const q = this.elapsedTime / this.intervalInMs;
this._value = this.interpolator(this.from, this.to, q);
this.onChange?.call(null, this._value);
}
public get value(): T {
return this._value;
}
}

View file

@ -2,5 +2,5 @@ import './background.scss';
import { html } from '../../types/html';
export const generate = (): html => `
<canvas id="background"></canvas>
<div id="background"></div>
`;

View file

@ -1,12 +1,20 @@
@use '../../style/mixins' as *;
@use '../../style/dark-mode/dark-mode' as *;
canvas#background {
position: fixed;
top: 0;
.blob {
position: absolute;
left: 0;
height: 100%;
width: 100%;
z-index: -10;
top: 0;
border-radius: 1000px;
transition: background-color var(--transition-time);
&:nth-child(odd) {
background-color: #fff9e0;
}
&:nth-child(even) {
background-color: #ffd6d6;
}
@media print {
& {
@ -14,3 +22,9 @@ canvas#background {
}
}
}
@include in-dark-mode {
.blob {
background-color: #2c477a;
}
}

View file

@ -1,160 +1,121 @@
import { PageElement } from '../page-element';
import { Blob } from './blob';
import { generate } from './background.html';
import { Vec3 } from './vec3';
import { Vec2 } from './vec2';
import { createElement } from '../../helper/create-element';
import { sum } from '../../helper/sum';
import { getHeight } from '../../helper/get-height';
import { OnLoadEvent } from '../../events/concrete-events/on-load-event';
import { OptionalEvent } from '../../events/optional-event';
import { OnPageThemeChangedEvent } from '../../events/concrete-events/on-page-theme-changed-event';
import { mix } from '../../helper/mix';
import { Random } from '../../helper/random';
export class PageBackground extends PageElement {
public static readonly blobSpacing = 325;
public static readonly minBlobCount = 30;
public static readonly perspective = 5;
public static readonly zMin = 10;
public static readonly zMax = 30;
private static readonly perspective = 5;
private static readonly zMin = 6;
private static readonly zMax = 50;
private backgroundSize: Vec2;
private scrollPosition = 0;
private previousTimestamp: DOMHighResTimeStamp = null;
private readonly blobs: Array<Blob> = [];
private readonly canvas: HTMLCanvasElement;
private readonly ctx: CanvasRenderingContext2D;
private parent: PageElement;
private random: Random = new Random();
private blobs: Array<HTMLElement> = [];
public constructor(
private readonly start: PageElement,
private readonly inBetween: Array<PageElement>,
private readonly end: PageElement
private readonly topOffsetElementCount: number,
private readonly bottomOffsetElementCount: number
) {
super(createElement(generate()));
this.canvas = this.htmlRoot as HTMLCanvasElement;
this.ctx = this.canvas.getContext('2d');
}
public handleOnLoadEvent(event: OnLoadEvent): OptionalEvent {
this.parent = event.parent;
requestAnimationFrame(this.draw.bind(this));
return super.handleOnLoadEvent(event);
}
public handleOnPageThemeChangedEvent(event: OnPageThemeChangedEvent): OptionalEvent {
Blob.changeTheme(event.isDark);
this.blobs.forEach(b => b.decideColor());
return super.handleOnPageThemeChangedEvent(event);
}
private createBlobs() {
const requiredBlobCount = Math.max(
PageBackground.minBlobCount,
(this.backgroundSize.x * this.backgroundSize.y) / PageBackground.blobSpacing ** 2
);
while (requiredBlobCount > this.blobs.length) {
this.blobs.push(new Blob());
for (let i = 0; i < window.innerWidth / 10; i++) {
const blob = document.createElement('div');
blob.classList.add('blob');
blob.style.width = '140px';
const z = this.random.inInterval(PageBackground.zMin, PageBackground.zMax);
blob.style.zIndex = (-z).toFixed(0);
blob.style.opacity = (
1 -
(z - PageBackground.zMin) / (PageBackground.zMax - PageBackground.zMin)
).toString();
blob.style.height = `${this.random.inInterval(360, 740)}px`;
this.blobs.push(blob);
this.htmlRoot.appendChild(blob);
}
}
private resizeCanvas() {
this.canvas.width = this.canvas.clientWidth;
this.canvas.height = this.canvas.clientHeight;
}
private windowHeight = 0;
private windowWidth = 0;
private contentHeight = 0;
private drawIfNecessary() {
const siblings = this.getSiblings();
const currentContentHeight = sum(siblings.map(getHeight));
private resizeBackground() {
const targetWidth = this.parent.htmlRoot.clientWidth;
if (
window.innerWidth !== this.windowWidth ||
window.innerHeight !== this.windowHeight ||
currentContentHeight !== this.contentHeight
) {
this.windowWidth = window.innerWidth;
this.windowHeight = window.innerHeight;
this.contentHeight = currentContentHeight;
const siblings: Array<HTMLElement> = this.getSiblings();
const targetHeight = sum(siblings.map(getHeight));
if (targetWidth === this.canvas.width && targetHeight === this.canvas.height) {
return;
}
const targetSize = new Vec2(targetWidth, targetHeight);
this.backgroundSize = targetSize;
this.blobs.forEach(blob => {
const variableOffset = (offset, q) =>
Math.max(
0,
offset -
((blob.z - PageBackground.zMin) /
(PageBackground.zMax - PageBackground.zMin)) *
offset *
q
);
const topOffset = variableOffset(getHeight(this.start.htmlRoot), 1);
const topLeft = this.convertFrom2Dto3D(new Vec2(0, topOffset), blob.z);
const bottomOffset = variableOffset(getHeight(this.end.htmlRoot), 0.2);
const bottomRight = this.convertFrom2Dto3D(
new Vec2(this.canvas.width, this.canvas.height - bottomOffset),
blob.z,
targetSize.y - this.canvas.height
this.randomizeBlobs(
sum(siblings.slice(0, this.topOffsetElementCount).map(getHeight)),
sum(siblings.slice(-this.bottomOffsetElementCount).map(getHeight))
);
}
blob.positionOffset = topLeft;
blob.positionScale = bottomRight.subtract(topLeft);
});
requestAnimationFrame(this.drawIfNecessary.bind(this));
}
private parent?: HTMLElement;
protected setParent(parent: PageElement) {
this.parent = parent.htmlRoot;
requestAnimationFrame(this.drawIfNecessary.bind(this));
super.setParent(parent);
}
private getSiblings(): Array<HTMLElement> {
return [this.start, ...this.inBetween, this.end].map(e => e.htmlRoot);
return Array.prototype.slice
.call(this.parent!.childNodes)
.filter((n: HTMLElement) => n !== this.htmlRoot);
}
private draw(timestamp: DOMHighResTimeStamp) {
this.resizeCanvas();
this.resizeBackground();
this.createBlobs();
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const deltaTime = this.getDeltaTime(timestamp);
this.blobs.forEach(b => b.step(deltaTime));
this.scrollPosition = this.parent.htmlRoot.scrollTop;
this.blobs.sort((b1, b2) => b2.z - b1.z);
this.blobs.forEach(blob => {
const topLeft = this.convertFrom3Dto2D(blob.topLeft);
const bottomRight = this.convertFrom3Dto2D(
blob.topLeft.add(Vec3.from(blob.size, 0))
);
if (this.isInView(topLeft, bottomRight)) {
blob.draw(this.ctx, topLeft, bottomRight.subtract(topLeft));
}
private randomizeBlobs(topOffset: number, bottomOffset: number) {
this.random.seed = 50;
this.blobs.forEach(b => {
const z = -Number.parseInt(b.style.zIndex);
const [x, y] = this.randomXY(z, topOffset, bottomOffset);
b.style.transform = `translate3D(${x}px, ${y}px, ${-z}px) rotate(-20deg)`;
});
requestAnimationFrame(this.draw.bind(this));
}
private getDeltaTime(timestamp: DOMHighResTimeStamp): number {
const deltaTime = this.previousTimestamp ? timestamp - this.previousTimestamp : 0;
this.previousTimestamp = timestamp;
return Math.max(0, deltaTime);
}
private convertFrom3Dto2D(p: Vec3): Vec2 {
const m = PageBackground.perspective / (PageBackground.perspective + p.z);
return new Vec2(m * (p.z / 2 + p.x), m * (p.z / 2 + p.y - this.scrollPosition));
}
private convertFrom2Dto3D(p: Vec2, z: number, scrollPosition = 0): Vec2 {
const m = 1 + z / PageBackground.perspective;
return new Vec2(p.x * m - z / 2, p.y * m - z / 2 + scrollPosition);
}
private isInView(topLeft: Vec2, bottomRight: Vec2): boolean {
return (
((0 <= topLeft.x && topLeft.x <= this.canvas.width) ||
(0 <= bottomRight.x && bottomRight.x < this.canvas.width)) &&
((0 <= topLeft.y && topLeft.y <= this.canvas.height) ||
(0 <= bottomRight.y && bottomRight.y <= this.canvas.height))
private randomXY(z: number, topOffset: number, bottomOffset: number): [number, number] {
const farTop = -(
((this.windowHeight / 2 - topOffset) / PageBackground.perspective) *
(PageBackground.zMax + PageBackground.perspective) -
this.windowHeight / 2
);
const farBottom =
((this.windowHeight / 2 - bottomOffset) / PageBackground.perspective) *
(PageBackground.zMax + PageBackground.perspective) -
this.windowHeight / 2 +
this.contentHeight;
const endXSpan =
((this.windowWidth / PageBackground.perspective) *
(PageBackground.zMax + PageBackground.perspective)) /
2;
return [
this.random.inInterval(
mix(0, -(endXSpan - this.windowWidth / 2), z / PageBackground.zMax),
mix(
this.windowWidth,
this.windowWidth + endXSpan - this.windowWidth / 2,
z / PageBackground.zMax
)
),
this.random.inInterval(
mix(topOffset, farTop, z / PageBackground.zMax),
mix(this.contentHeight - bottomOffset, farBottom, z / PageBackground.zMax)
),
];
}
}

View file

@ -1,100 +0,0 @@
import { Vec2 } from './vec2';
import { Vec3 } from './vec3';
import { Random } from '../../helper/random';
import { Animation } from './animation';
import { PageBackground } from './background';
import { mix } from '../../helper/mix';
export class Blob {
private static readonly darkColors = [new Vec3(44, 71, 122)];
private static readonly lightColors = [
new Vec3(255, 249, 224),
new Vec3(255, 214, 214),
];
private static readonly creatorRandom = new Random(51);
private static colorPickerRandom = new Random(132);
private static isDarkThemed = false;
public static changeTheme(isDarkThemed: boolean) {
Blob.colorPickerRandom = new Random(132);
Blob.isDarkThemed = isDarkThemed;
}
public readonly z = Blob.creatorRandom.randomInInterval(
PageBackground.zMin,
PageBackground.zMax
);
private color: Animation<Vec3>;
private readonly positionQ = new Vec2(Blob.creatorRandom.next, Blob.creatorRandom.next);
private _positionScale = new Vec2(0, 0);
private _positionOffset = new Vec2(0, 0);
private opacity: number;
private readonly _size = new Vec2(140, Blob.creatorRandom.randomInInterval(260, 740));
public constructor() {
this.opacity =
1 - (this.z - PageBackground.zMin) / (PageBackground.zMax - PageBackground.zMin);
this.decideColor();
}
public decideColor() {
const target = Blob.colorPickerRandom.choose(
Blob.isDarkThemed ? Blob.darkColors : Blob.lightColors
);
this.color = new Animation<Vec3>(
this.color ? this.color.value : target,
target,
125,
(f, t, q) => {
return new Vec3(mix(f.x, t.x, q), mix(f.y, t.y, q), mix(f.z, t.z, q));
}
);
}
public step(deltaTime: number) {
this.color?.step(deltaTime);
}
public get topLeft(): Vec3 {
return Vec3.from(
this.positionQ.multiply(this._positionScale).add(this._positionOffset),
this.z
);
}
public get size(): Vec2 {
return this._size;
}
public set positionScale(value: Vec2) {
this._positionScale = value;
}
public set positionOffset(value: Vec2) {
this._positionOffset = value;
}
public draw(ctx: CanvasRenderingContext2D, position: Vec2, size: Vec2) {
ctx.save();
ctx.translate(position.x, position.y);
ctx.rotate((-20 / 180) * Math.PI);
ctx.beginPath();
ctx.arc(0, size.x / 2, size.x / 2, Math.PI, 0);
ctx.arc(0, size.y - size.x / 2, size.x / 2, 0, Math.PI);
ctx.closePath();
const { x, y, z } = this.color.value;
ctx.fillStyle = `rgba(${x}, ${y}, ${z}, ${this.opacity})`;
ctx.fill();
ctx.restore();
}
}

View file

@ -1,17 +0,0 @@
export class Vec2 {
public static readonly Zero = new Vec2(0, 0);
public constructor(public readonly x: number, public readonly y: number) {}
public add(other: Vec2): Vec2 {
return new Vec2(this.x + other.x, this.y + other.y);
}
public subtract(other: Vec2): Vec2 {
return new Vec2(this.x - other.x, this.y - other.y);
}
public multiply(other: Vec2): Vec2 {
return new Vec2(this.x * other.x, this.y * other.y);
}
}

View file

@ -1,23 +0,0 @@
import { Vec2 } from './vec2';
export class Vec3 {
public static readonly Zero = new Vec3(0, 0, 0);
public static from(vec2: Vec2, z: number): Vec3 {
return new Vec3(vec2.x, vec2.y, z);
}
public constructor(
public readonly x: number,
public readonly y: number,
public readonly z: number
) {}
public add(other: Vec3): Vec3 {
return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z);
}
public multiply(other: Vec3): Vec3 {
return new Vec3(this.x * other.x, this.y * other.y, this.z * other.z);
}
}

View file

@ -1,13 +1,9 @@
import { PageElement } from '../page-element';
import { OnLoadEvent } from '../../events/concrete-events/on-load-event';
import { OnEventBroadcasterChangedEvent } from '../../events/concrete-events/on-event-broadcaster-changed-event';
export class Body extends PageElement {
constructor(root: HTMLElement, children: Array<PageElement>) {
super(root);
constructor(...children: Array<PageElement>) {
super(document.body, children);
children.forEach(c => this.attachElement(c));
this.broadcastEvent(new OnEventBroadcasterChangedEvent(this));
this.broadcastEvent(new OnLoadEvent(this));
this.setParent();
}
}

View file

@ -0,0 +1,6 @@
import './main.scss';
import { html } from '../../types/html';
export const generate = (): html => `
<main></main>
`;

21
src/page/main/main.scss Normal file
View file

@ -0,0 +1,21 @@
@use '../../style/mixins' as *;
main {
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
perspective: 5px;
@media (hover: hover) {
&::-webkit-scrollbar-track,
&::-webkit-scrollbar {
background-color: transparent;
width: 12px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--accent-color);
border-radius: var(--border-radius);
}
}
}

10
src/page/main/main.ts Normal file
View file

@ -0,0 +1,10 @@
import { PageElement } from '../page-element';
import { generate } from './main.html';
import { createElement } from '../../helper/create-element';
export class Main extends PageElement {
constructor(...children: Array<PageElement>) {
super(createElement(generate()), children);
children.forEach(c => this.attachElement(c));
}
}

View file

@ -1,46 +1,21 @@
import { EventHandler } from '../events/event-handler';
import { EventBroadcaster } from '../events/event-broadcaster';
import { OnEventBroadcasterChangedEvent } from '../events/concrete-events/on-event-broadcaster-changed-event';
import { OptionalEvent } from '../events/optional-event';
import { Event } from '../events/event';
import { OnLoadEvent } from '../events/concrete-events/on-load-event';
export abstract class PageElement extends EventHandler implements EventBroadcaster {
protected eventBroadcaster: EventBroadcaster;
export abstract class PageElement {
public constructor(
public readonly htmlRoot?: HTMLElement,
public readonly htmlRoot: HTMLElement,
protected children: Array<PageElement> = []
) {
super();
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected setParent(parent?: PageElement): void {
this.children.forEach(c => c.setParent(this));
}
public broadcastEvent(event: Event) {
event = this.handle(event);
if (event) {
this.children.forEach(c => c.broadcastEvent(event));
}
}
public handleOnEventBroadcasterChangedEvent(
event: OnEventBroadcasterChangedEvent
): OptionalEvent {
this.eventBroadcaster = event.broadcaster;
return super.handleOnEventBroadcasterChangedEvent(event);
}
public handleOnLoadEvent(_: OnLoadEvent): OptionalEvent {
return super.handleOnLoadEvent(new OnLoadEvent(this));
}
protected query(query: string): HTMLElement | null {
return this.htmlRoot?.querySelector(query);
protected query(query: string): HTMLElement {
return this.htmlRoot.querySelector(query) as HTMLElement;
}
protected attachElementByReplacing(query: string, element: PageElement) {
const old = this.query(query);
old.parentElement.replaceChild(element.htmlRoot, old);
old.parentElement!.replaceChild(element.htmlRoot, old);
this.children.push(element);
}

View file

@ -8,7 +8,7 @@ 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 { Main } from './page/main/main';
import { ImageAnchorFactory } from './page/basics/image-anchor/image-anchor';
import { Preview } from './page/basics/preview/preview';
@ -59,328 +59,331 @@ import ledWebM from './static/media/led.webm';
import githubIcon from './static/icons/github.svg';
import openIcon from './static/icons/open.svg';
import cvIcon from './static/icons/cv.svg';
import { Body } from './page/body/body';
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 header = new PageHeader({
name: `András Schmelczer`,
picture: new Image(meWebP, meJpeg, `a picture of me`, false),
about: [
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 lifes 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.`),
],
});
new Body(
new Main(
new PageBackground(1, 1),
new PageHeader({
name: `András Schmelczer`,
picture: new Image(meWebP, meJpeg, `a picture of me`, false),
about: [
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 lifes 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.`),
],
}),
new PageTimeline({
showMoreText: `Show details`,
showLessText: `Show less`,
elements: [
{
title: `Multiplayer game`,
date: `2020 Autumn`,
figure: new Preview(
declaredWebP,
declaredJpeg,
'https://decla.red',
'The website of the video game'
),
description: new Text(
`Using SDF-2D, I developed a conquest-style multiplayer browser game. It even runs on mobiles.`
),
more: [
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(
sdf2dWebP,
sdf2dJpeg,
'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.`),
],
links: [
new GitHub('https://github.com/schmelczerandras/sdf-2d'),
new Open('https://sdf2d.schmelczer.dev'),
],
},
{
title: `Video game on an ATtiny85`,
date: `2020 Spring`,
figure: new Video({
poster: last(adAstraPoster.images)!.path,
mp4: adAstraMp4,
webm: adAstraWebM,
options: `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.`),
more: [
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.
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.`
),
],
links: [new GitHub('https://github.com/schmelczerandras/ad_astra')],
},
const timeline = new PageTimeline({
showMoreText: `Show details`,
showLessText: `Show less`,
elements: [
{
title: `Multiplayer game`,
date: `2020 Autumn`,
figure: new Preview(
declaredWebP,
declaredJpeg,
'https://decla.red',
'The website of the video game'
),
description: new Text(
`Using SDF-2D, I developed a conquest-style multiplayer browser game. It even runs on mobiles.`
),
more: [
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.`),
{
title: `Predicting foreign exchange rates`,
date: `2019 Autumn`,
figure: new Video({
mp4: forexMp4,
webm: forexWebM,
options: `autoplay loop muted playsinline controls`,
}),
description: new Text(
`From the animation we can see that my algorithm does a somewhat acceptable job at
predicting (blue graph) the EUR/USD rates (green graph).`
),
more: [
new Text(
`In a nutshell, the algorithm (written with Python - NumPy, SciPy, Flask),
extrapolates in the frequency domain. The steps are the following: smoothing the input values,
differentiating, applying a short-time Fourier-transformation with overlapped (and Hanning-windowed) windows,
extrapolating and then applying the inverse of these transformations to the extrapolated values.`
),
new Text(
`Of course, there is still plenty of room for improvement, but even with this simple algorithm
a mostly profitable trading strategy is viable. In my free time I may put more work into it.`
),
],
links: [],
},
{
date: `2019 November`,
title: `My Notes`,
figure: new Image(
myNotesWebP,
myNotesJpeg,
`two screenshots of the application`
),
description: new Text(
`A minimalist note organizer and editor powered by Markwon.`
),
more: [
new Text(
`A basic android app for creating and filtering notes written in markdown.`
),
new Anchor(
`https://github.com/schmelczerandras/my-notes`,
`MyNotes on GitHub`
),
new Text(
`It was my homework for BME's Android and web development course.
It was also my first experience with Android development.`
),
],
links: [],
},
{
date: `2018 October - November`,
title: `Simulating the cooling system of a nuclear facility`,
figure: new Image(
processSimulatorWebP,
processSimulatorJpeg,
`a screenshot of the simulator`
),
description: new Text(
`Dynamically calculating the temperatures and flow velocities
in a fluid-based cooling system based on a simple model.`
),
more: [
new Text(
`A simulated system can contain reactors (heaters / coolers), pumps, heat exchangers,
drains sources, and of course, pipes.`
),
new Text(
`The algorithm takes advantages of graphs and matrices to get to a next time frame.`
),
new Text(
`Python is used for the backend along with Flask and NumPy. A REST API facilitates
the communication between the layers. For drawing the frontend HTML5 canvas is utilized.`
),
],
links: [],
},
{
date: `2018 October - November`,
title: `Graph editing application`,
figure: new Image(
processSimulatorInputWebP,
processSimulatorInputJpeg,
`a picture of the simulator's UI`
),
description: new Text(
`An intuitive editor to create and edit input files for the nuclear facility simulator.`
),
more: [
new Text(
`Nodes can be moved with drag&drop gestures. Editing the parameters of elements
can be done on the right panel.`
),
new Text(
`The UI is built with JavaFX. The output can be exported as JSON or
directly uploaded to the simulation backend.`
),
],
links: [],
},
{
date: `2018 July - August`,
title: `City simulation`,
figure: new Image(
citySimulationWebP,
citySimulationJpeg,
`a picture of a low-poly city`
),
description: new Text(
`Simulating a city where car crashes are more frequent than usual.`
),
more: [
new Text(
`Through a REST API the state of the traffic lights can be changed.
The 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.`
),
new Text(
`This was created for a Cybersecurity challenge. With the help of this program
the contestants could instantly see the effect of their work.`
),
new Text(
`The most interesting aspect of this project was building it in a server-client architecture.
The decisions of the agents is calculated server-side. The real challenge was broadcasting
these decisions in a fault-tolerant way using minimal bandwidth.`
),
new Text(
`The program is made with Unity using C# as the scripting language. The models and animations
were also made by me using Blender.`
),
],
links: [],
},
{
date: `2018 June`,
title: `Photo colour grader`,
figure: new Image(colourWebP, colourJpeg, `a picture of the app`),
description: new Text(
`An innovative (at least I thought so) colour grader web application.`
),
more: [
new Text(
`The most noteworthy feature of this application is the colour selector UI.
This program is only intended as a proof-of-concept, I wanted to experiment with
some ideas and this was the outcome.`
),
new Text(
`You can select some colours and then apply transformations to the other colours as a
function of their distance to the selected colour.`
),
new Text(
`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).`
),
],
links: [new Open('color.schmelczer.dev')],
},
{
date: `2017 autumn`,
title: `Platform game`,
figure: new Image(platformWebP, platformJpeg, `a picture of the app`),
description: new Text(
`A 3D game written in C with the help of SDL 1.2 (I haven't heard of GPU programming at the time).`
),
more: [
new Text(
`The maps are randomly generated and fully destroyable.
The player is getting chased by flying enemies. Overall, I find it a really enjoyable game.`
),
new Text(`I did this as a homework for my Basics of Programming course.`),
],
links: [],
},
{
date: `2016 summer`,
title: `Photos`,
figure: new Image(photosWebP, photosJpeg, `a picture of the website`),
description: new Text(`A simple web page where you can view my photos.`),
links: [new Open('https://photo.schmelczer.dev')],
},
{
date: `2016 spring`,
title: `Lights synchronised to music`,
figure: new Video({
poster: last(ledPoster.images)!.path,
mp4: ledMp4,
webm: ledWebM,
options: `controls playsinline preload="none"`,
}),
description: new Text(
`A full stack application with a built-in
music player which music controls the colour of some RGB LED strips.`
),
more: [
new Text(
`This was my first non-trivial project which got finished. Obviously,
it is rather far from perfect, but I am still proud that I was able to build it on my own.`
),
new Text(
`The backend logic is written in Python, the FFT implementation is provided by NumPy.
A quite simple frontend for accessing the music player and changing
the settings also got built using vanilla web development technologies.`
),
],
links: [],
},
],
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(
sdf2dWebP,
sdf2dJpeg,
'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.`),
],
links: [
new GitHub('https://github.com/schmelczerandras/sdf-2d'),
new Open('https://sdf2d.schmelczer.dev'),
],
},
{
title: `Video game on an ATtiny85`,
date: `2020 Spring`,
figure: new Video(
last(adAstraPoster.images).path,
adAstraMp4,
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.`),
more: [
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.
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.`
),
],
links: [new GitHub('https://github.com/schmelczerandras/ad_astra')],
},
{
title: `Predicting foreign exchange rates`,
date: `2019 Autumn`,
figure: new Video(
null,
forexMp4,
forexWebM,
`autoplay loop muted playsinline controls`
),
description: new Text(
`From the animation we can see that my algorithm does a somewhat acceptable job at
predicting (blue graph) the EUR/USD rates (green graph).`
),
more: [
new Text(
`In a nutshell, the algorithm (written with Python - NumPy, SciPy, Flask),
extrapolates in the frequency domain. The steps are the following: smoothing the input values,
differentiating, applying a short-time Fourier-transformation with overlapped (and Hanning-windowed) windows,
extrapolating and then applying the inverse of these transformations to the extrapolated values.`
),
new Text(
`Of course, there is still plenty of room for improvement, but even with this simple algorithm
a mostly profitable trading strategy is viable. In my free time I may put more work into it.`
),
],
links: [],
},
{
date: `2019 November`,
title: `My Notes`,
figure: new Image(myNotesWebP, myNotesJpeg, `two screenshots of the application`),
description: new Text(
`A minimalist note organizer and editor powered by Markwon.`
),
more: [
new Text(
`A basic android app for creating and filtering notes written in markdown.`
),
new Anchor(`https://github.com/schmelczerandras/my-notes`, `MyNotes on GitHub`),
new Text(
`It was my homework for BME's Android and web development course.
It was also my first experience with Android development.`
),
],
links: [],
},
{
date: `2018 October - November`,
title: `Simulating the cooling system of a nuclear facility`,
figure: new Image(
processSimulatorWebP,
processSimulatorJpeg,
`a screenshot of the simulator`
),
description: new Text(
`Dynamically calculating the temperatures and flow velocities
in a fluid-based cooling system based on a simple model.`
),
more: [
new Text(
`A simulated system can contain reactors (heaters / coolers), pumps, heat exchangers,
drains sources, and of course, pipes.`
),
new Text(
`The algorithm takes advantages of graphs and matrices to get to a next time frame.`
),
new Text(
`Python is used for the backend along with Flask and NumPy. A REST API facilitates
the communication between the layers. For drawing the frontend HTML5 canvas is utilized.`
),
],
links: [],
},
{
date: `2018 October - November`,
title: `Graph editing application`,
figure: new Image(
processSimulatorInputWebP,
processSimulatorInputJpeg,
`a picture of the simulator's UI`
),
description: new Text(
`An intuitive editor to create and edit input files for the nuclear facility simulator.`
),
more: [
new Text(
`Nodes can be moved with drag&drop gestures. Editing the parameters of elements
can be done on the right panel.`
),
new Text(
`The UI is built with JavaFX. The output can be exported as JSON or
directly uploaded to the simulation backend.`
),
],
links: [],
},
{
date: `2018 July - August`,
title: `City simulation`,
figure: new Image(
citySimulationWebP,
citySimulationJpeg,
`a picture of a low-poly city`
),
description: new Text(
`Simulating a city where car crashes are more frequent than usual.`
),
more: [
new Text(
`Through a REST API the state of the traffic lights can be changed.
The 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.`
),
new Text(
`This was created for a Cybersecurity challenge. With the help of this program
the contestants could instantly see the effect of their work.`
),
new Text(
`The most interesting aspect of this project was building it in a server-client architecture.
The decisions of the agents is calculated server-side. The real challenge was broadcasting
these decisions in a fault-tolerant way using minimal bandwidth.`
),
new Text(
`The program is made with Unity using C# as the scripting language. The models and animations
were also made by me using Blender.`
),
],
links: [],
},
{
date: `2018 June`,
title: `Photo colour grader`,
figure: new Image(colourWebP, colourJpeg, `a picture of the app`),
description: new Text(
`An innovative (at least I thought so) colour grader web application.`
),
more: [
new Text(
`The most noteworthy feature of this application is the colour selector UI.
This program is only intended as a proof-of-concept, I wanted to experiment with
some ideas and this was the outcome.`
),
new Text(
`You can select some colours and then apply transformations to the other colours as a
function of their distance to the selected colour.`
),
new Text(
`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).`
),
],
links: [new Open('color.schmelczer.dev')],
},
{
date: `2017 autumn`,
title: `Platform game`,
figure: new Image(platformWebP, platformJpeg, `a picture of the app`),
description: new Text(
`A 3D game written in C with the help of SDL 1.2 (I haven't heard of GPU programming at the time).`
),
more: [
new Text(
`The maps are randomly generated and fully destroyable.
The player is getting chased by flying enemies. Overall, I find it a really enjoyable game.`
),
new Text(`I did this as a homework for my Basics of Programming course.`),
],
links: [],
},
{
date: `2016 summer`,
title: `Photos`,
figure: new Image(photosWebP, photosJpeg, `a picture of the website`),
description: new Text(`A simple web page where you can view my photos.`),
links: [new Open('https://photo.schmelczer.dev')],
},
{
date: `2016 spring`,
title: `Lights synchronised to music`,
figure: new Video(
last(ledPoster.images).path,
ledMp4,
ledWebM,
`controls playsinline preload="none"`
),
description: new Text(
`A full stack application with a built-in
music player which music controls the colour of some RGB LED strips.`
),
more: [
new Text(
`This was my first non-trivial project which got finished. Obviously,
it is rather far from perfect, but I am still proud that I was able to build it on my own.`
),
new Text(
`The backend logic is written in Python the FFT is provided by NumPy.
A quite simple frontend for accessing the music player and changing
the settings also got built using vanilla web development technologies.`
),
],
links: [],
},
],
});
const footer = new PageFooter({
title: `Learn more`,
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
});
new Body(document.querySelector('main'), [
new PageImageViewer(),
header,
timeline,
footer,
new PageBackground(header, [timeline], footer),
]);
}),
new PageFooter({
title: `Learn more`,
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
})
),
new PageImageViewer()
);
};

View file

@ -3,50 +3,47 @@
@use 'style/animations/animations';
@use 'style/dark-mode/dark-mode';
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
height: 100%;
transition: background-color linear var(--transition-time);
@include on-small-screen {
font-size: 0.8rem;
}
@media print {
& {
font-size: 0.75rem;
font-size: 0.7rem;
}
}
}
:focus {
outline: none;
body {
background-color: var(--background);
transition: background-color linear var(--transition-time);
&:not(:hover) {
outline: var(--accent-color) solid 2px;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
env(safe-area-inset-left);
height: 100%;
@media print {
& {
height: auto;
}
}
}
::-moz-selection {
background-color: var(--accent-color);
color: var(--very-light-text-color);
}
::selection {
background-color: var(--accent-color);
color: var(--very-light-text-color);
}
*,
*::before,
*::after {
@include main-font();
margin: 0;
padding: 0;
box-sizing: border-box;
transition: background-color linear var(--transition-time), color var(--transition-time);
hyphens: auto;
noscript {
@include square(100%);
@include center-children();
}
.figure-container {
@ -61,46 +58,27 @@ html {
iframe {
pointer-events: all;
position: relative;
z-index: -2;
z-index: -1;
width: 100%;
height: auto;
}
}
body {
background-color: var(--background);
img,
video,
iframe {
user-select: none;
}
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
env(safe-area-inset-left);
:focus {
outline: none;
height: 100%;
@media print {
& {
height: auto;
}
}
main {
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
noscript {
@include square(100%);
@include center-children();
}
@media (hover: hover) {
&::-webkit-scrollbar-track,
&::-webkit-scrollbar {
background-color: transparent;
width: 12px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--accent-color);
border-radius: var(--border-radius);
}
}
&:not(:hover) {
outline: var(--accent-color) solid 2px;
}
}
::selection {
background-color: var(--accent-color);
color: var(--very-light-text-color);
}