From 91d92f7f48129186068aa0f1765bddcafabc4bcc Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Sun, 22 Nov 2020 22:41:10 +0100 Subject: [PATCH] Fix background and fix PageElement system --- .vscode/settings.json | 2 + .../on-event-broadcaster-changed-event.ts | 12 - src/events/concrete-events/on-load-event.ts | 12 - .../on-page-theme-changed-event.ts | 11 - src/events/event-broadcaster.ts | 5 - src/events/event-handler.ts | 26 - src/events/event.ts | 6 - src/events/optional-event.ts | 3 - src/helper/random.ts | 6 +- src/index.html | 10 +- src/page/background/animation.ts | 30 - src/page/background/background.html.ts | 2 +- src/page/background/background.scss | 26 +- src/page/background/background.ts | 217 +++--- src/page/background/blob.ts | 100 --- src/page/background/vec2.ts | 17 - src/page/background/vec3.ts | 23 - src/page/body/body.ts | 10 +- src/page/main/main.html.ts | 6 + src/page/main/main.scss | 21 + src/page/main/main.ts | 10 + src/page/page-element.ts | 45 +- src/portfolio.ts | 637 +++++++++--------- src/styles.scss | 100 ++- 24 files changed, 528 insertions(+), 809 deletions(-) delete mode 100644 src/events/concrete-events/on-event-broadcaster-changed-event.ts delete mode 100644 src/events/concrete-events/on-load-event.ts delete mode 100644 src/events/concrete-events/on-page-theme-changed-event.ts delete mode 100644 src/events/event-broadcaster.ts delete mode 100644 src/events/event-handler.ts delete mode 100644 src/events/event.ts delete mode 100644 src/events/optional-event.ts delete mode 100644 src/page/background/animation.ts delete mode 100644 src/page/background/blob.ts delete mode 100644 src/page/background/vec2.ts delete mode 100644 src/page/background/vec3.ts create mode 100644 src/page/main/main.html.ts create mode 100644 src/page/main/main.scss create mode 100644 src/page/main/main.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index c050d5b..48032d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,11 @@ { "cSpell.words": [ + "Glsl", "andras", "decla", "favicons", "forex", + "froms", "schmelczer", "webm", "webp" diff --git a/src/events/concrete-events/on-event-broadcaster-changed-event.ts b/src/events/concrete-events/on-event-broadcaster-changed-event.ts deleted file mode 100644 index f42fa90..0000000 --- a/src/events/concrete-events/on-event-broadcaster-changed-event.ts +++ /dev/null @@ -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); - } -} diff --git a/src/events/concrete-events/on-load-event.ts b/src/events/concrete-events/on-load-event.ts deleted file mode 100644 index 1be1eb0..0000000 --- a/src/events/concrete-events/on-load-event.ts +++ /dev/null @@ -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); - } -} diff --git a/src/events/concrete-events/on-page-theme-changed-event.ts b/src/events/concrete-events/on-page-theme-changed-event.ts deleted file mode 100644 index caabf82..0000000 --- a/src/events/concrete-events/on-page-theme-changed-event.ts +++ /dev/null @@ -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); - } -} diff --git a/src/events/event-broadcaster.ts b/src/events/event-broadcaster.ts deleted file mode 100644 index e4d943d..0000000 --- a/src/events/event-broadcaster.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Event } from './event'; - -export interface EventBroadcaster { - broadcastEvent(event: Event): void; -} diff --git a/src/events/event-handler.ts b/src/events/event-handler.ts deleted file mode 100644 index 97ef5bc..0000000 --- a/src/events/event-handler.ts +++ /dev/null @@ -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; - } -} diff --git a/src/events/event.ts b/src/events/event.ts deleted file mode 100644 index c07b72e..0000000 --- a/src/events/event.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { EventHandler } from './event-handler'; -import { OptionalEvent } from './optional-event'; - -export interface Event { - accept(handler: EventHandler): OptionalEvent; -} diff --git a/src/events/optional-event.ts b/src/events/optional-event.ts deleted file mode 100644 index edad78f..0000000 --- a/src/events/optional-event.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Event } from './event'; - -export type OptionalEvent = Event | undefined; diff --git a/src/helper/random.ts b/src/helper/random.ts index 9509744..236463d 100644 --- a/src/helper/random.ts +++ b/src/helper/random.ts @@ -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(list: Array): 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; } } diff --git a/src/index.html b/src/index.html index 0800150..a0f649e 100644 --- a/src/index.html +++ b/src/index.html @@ -5,10 +5,10 @@ - + + - @@ -30,11 +30,9 @@ rel="stylesheet" /> - András Schmelczer - Portfolio + Portfolio - András Schmelczer -
- -
+ diff --git a/src/page/background/animation.ts b/src/page/background/animation.ts deleted file mode 100644 index a3c118e..0000000 --- a/src/page/background/animation.ts +++ /dev/null @@ -1,30 +0,0 @@ -export class Animation { - 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; - } -} diff --git a/src/page/background/background.html.ts b/src/page/background/background.html.ts index bdf0517..1205e18 100644 --- a/src/page/background/background.html.ts +++ b/src/page/background/background.html.ts @@ -2,5 +2,5 @@ import './background.scss'; import { html } from '../../types/html'; export const generate = (): html => ` - +
`; diff --git a/src/page/background/background.scss b/src/page/background/background.scss index ae1c3af..e92879e 100644 --- a/src/page/background/background.scss +++ b/src/page/background/background.scss @@ -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; + } +} diff --git a/src/page/background/background.ts b/src/page/background/background.ts index 29f7cde..8c1c557 100644 --- a/src/page/background/background.ts +++ b/src/page/background/background.ts @@ -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 = []; - private readonly canvas: HTMLCanvasElement; - private readonly ctx: CanvasRenderingContext2D; - private parent: PageElement; + private random: Random = new Random(); + private blobs: Array = []; public constructor( - private readonly start: PageElement, - private readonly inBetween: Array, - 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 = 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 { - 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) + ), + ]; } } diff --git a/src/page/background/blob.ts b/src/page/background/blob.ts deleted file mode 100644 index e9bf25d..0000000 --- a/src/page/background/blob.ts +++ /dev/null @@ -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; - - 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( - 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(); - } -} diff --git a/src/page/background/vec2.ts b/src/page/background/vec2.ts deleted file mode 100644 index a7be2ac..0000000 --- a/src/page/background/vec2.ts +++ /dev/null @@ -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); - } -} diff --git a/src/page/background/vec3.ts b/src/page/background/vec3.ts deleted file mode 100644 index 00af336..0000000 --- a/src/page/background/vec3.ts +++ /dev/null @@ -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); - } -} diff --git a/src/page/body/body.ts b/src/page/body/body.ts index a2fdb75..1eb81cc 100644 --- a/src/page/body/body.ts +++ b/src/page/body/body.ts @@ -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) { - super(root); + constructor(...children: Array) { + super(document.body, children); children.forEach(c => this.attachElement(c)); - - this.broadcastEvent(new OnEventBroadcasterChangedEvent(this)); - this.broadcastEvent(new OnLoadEvent(this)); + this.setParent(); } } diff --git a/src/page/main/main.html.ts b/src/page/main/main.html.ts new file mode 100644 index 0000000..664e0f0 --- /dev/null +++ b/src/page/main/main.html.ts @@ -0,0 +1,6 @@ +import './main.scss'; +import { html } from '../../types/html'; + +export const generate = (): html => ` +
+`; diff --git a/src/page/main/main.scss b/src/page/main/main.scss new file mode 100644 index 0000000..07366f7 --- /dev/null +++ b/src/page/main/main.scss @@ -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); + } + } +} diff --git a/src/page/main/main.ts b/src/page/main/main.ts new file mode 100644 index 0000000..fa5e923 --- /dev/null +++ b/src/page/main/main.ts @@ -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) { + super(createElement(generate()), children); + children.forEach(c => this.attachElement(c)); + } +} diff --git a/src/page/page-element.ts b/src/page/page-element.ts index 001c11d..7c760fb 100644 --- a/src/page/page-element.ts +++ b/src/page/page-element.ts @@ -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 = [] - ) { - 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); } diff --git a/src/portfolio.ts b/src/portfolio.ts index 9010ebb..11c138f 100644 --- a/src/portfolio.ts +++ b/src/portfolio.ts @@ -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 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.`), - ], - }); + 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 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.`), + ], + }), + 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() + ); }; diff --git a/src/styles.scss b/src/styles.scss index d6f16b5..83f4398 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -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); +}