diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 8c5bf83..2d5b9b8 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,15 +2,30 @@ - - - - + + + + + + + + + + + + - + + + + + + + + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..65261d6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "singleQuote": true +} diff --git a/src/framework/helper.ts b/src/framework/helper.ts deleted file mode 100644 index d4f98c7..0000000 --- a/src/framework/helper.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { html } from "../model/misc"; - -export const createElement = (from: html): HTMLElement => { - const element: HTMLElement = document.createElement("div"); - element.innerHTML = from; - return element.firstElementChild as HTMLElement; -}; - -export const randomFactory = seed => () => - ((2 ** 31 - 1) & (seed = Math.imul(48271, seed))) / 2 ** 31; - -export const fixedSeedRandom = randomFactory(42); - -export const choose = ( - list: Array, - random: () => number = fixedSeedRandom -): T => list[randomInInterval(0, list.length, random)]; - -export const randomInInterval = ( - aClosed: number, - bOpen: number, - random: () => number = fixedSeedRandom -): number => Math.floor((bOpen - aClosed) * random()) + aClosed; - -export const sum = (list: ArrayLike): number => - Array.prototype.reduce.call(list, (a, sum) => a + sum, 0); - -export const getHeight = (e: HTMLElement): number => { - const computedStyle = window.getComputedStyle(e); - return ( - // ignores margin collapse - e.clientHeight + - parseInt(computedStyle.marginTop) + - parseInt(computedStyle.marginBottom) + - parseInt(computedStyle.borderTopWidth) + - parseInt(computedStyle.borderBottomWidth) - ); -}; - -export const mixColors = ( - hexColorA: string, - hexColorB: string, - qA: number -): string => { - const colorA = hexToRGB(normalizeHex(hexColorA)); - const colorB = hexToRGB(normalizeHex(hexColorB)); - const mixedColor: [number, number, number] = [ - colorA[0] * qA + colorB[0] * (1 - qA), - colorA[1] * qA + colorB[1] * (1 - qA), - colorA[2] * qA + colorB[2] * (1 - qA) - ]; - - return RGBToHex(mixedColor); -}; - -const normalizeHex = (hex: string): string => { - hex = hex.trim(); - if (hex.startsWith("#")) { - hex = hex.substr(1); - } - return hex; -}; - -const hexToRGB = (hex: string): [number, number, number] => { - const [r1, r2, g1, g2, b1, b2] = hex; - return [ - Number.parseInt(r1 + r2, 16), - Number.parseInt(g1 + g2, 16), - Number.parseInt(b1 + b2, 16) - ]; -}; - -const RGBToHex = (rgb: [number, number, number]): string => - rgb.map(n => Math.round(n).toString(16)).join(""); - -export const range = ({ - from = 0, - to = Infinity, - step = 1 -}: { - from?: number; - to?: number; - step?: number; -}): Iterable => { - return { - *[Symbol.iterator]() { - for (let i = from; i < to; yield i, i += step) {} - } - }; -}; - -export const last = (list: Array): T => - list.length > 0 ? list[list.length - 1] : undefined; diff --git a/src/framework/helper/color-mixer.ts b/src/framework/helper/color-mixer.ts new file mode 100644 index 0000000..0f73a2f --- /dev/null +++ b/src/framework/helper/color-mixer.ts @@ -0,0 +1,41 @@ +export const mixColors = ( + hexColorA: string, + hexColorB: string, + quantityA: number +): string => { + const colorA = hexToRGB(normalizeHex(hexColorA)); + const colorB = hexToRGB(normalizeHex(hexColorB)); + + const mixedColor: [number, number, number] = [ + mix(colorA[0], colorB[0], quantityA), + mix(colorA[1], colorB[1], quantityA), + mix(colorA[2], colorB[2], quantityA), + ]; + + return RGBToHex(mixedColor); +}; + +const hexToRGB = ([r1, r2, g1, g2, b1, b2]: string): [ + number, + number, + number +] => { + return [ + Number.parseInt(r1 + r2, 16), + Number.parseInt(g1 + g2, 16), + Number.parseInt(b1 + b2, 16), + ]; +}; + +const normalizeHex = (hex: string): string => { + hex = hex.trim(); + if (hex.startsWith('#')) { + hex = hex.substr(1); + } + return hex; +}; + +const mix = (a: number, b: number, q: number): number => a * q + b * (1 - q); + +const RGBToHex = (rgb: [number, number, number]): string => + '#' + rgb.map(n => Math.round(n).toString(16)).join(''); diff --git a/src/framework/helper/create-element.ts b/src/framework/helper/create-element.ts new file mode 100644 index 0000000..a3d28dd --- /dev/null +++ b/src/framework/helper/create-element.ts @@ -0,0 +1,7 @@ +import { html } from '../../model/misc'; + +export const createElement = (from: html): HTMLElement => { + const element: HTMLElement = document.createElement('div'); + element.innerHTML = from; + return element.firstElementChild as HTMLElement; +}; diff --git a/src/framework/helper/get-height.ts b/src/framework/helper/get-height.ts new file mode 100644 index 0000000..aaaf76d --- /dev/null +++ b/src/framework/helper/get-height.ts @@ -0,0 +1,11 @@ +export const getHeight = (e: HTMLElement): number => { + const computedStyle = window.getComputedStyle(e); + return ( + // ignores margin collapse + e.clientHeight + + parseInt(computedStyle.marginTop) + + parseInt(computedStyle.marginBottom) + + parseInt(computedStyle.borderTopWidth) + + parseInt(computedStyle.borderBottomWidth) + ); +}; diff --git a/src/framework/helper/last.ts b/src/framework/helper/last.ts new file mode 100644 index 0000000..a8b73be --- /dev/null +++ b/src/framework/helper/last.ts @@ -0,0 +1,2 @@ +export const last = (list: Array): T => + list.length > 0 ? list[list.length - 1] : undefined; diff --git a/src/framework/helper/random.ts b/src/framework/helper/random.ts new file mode 100644 index 0000000..e006521 --- /dev/null +++ b/src/framework/helper/random.ts @@ -0,0 +1,17 @@ +export class Random { + public constructor(private seed: number) {} + + public get next(): number { + return ( + ((2 ** 31 - 1) & (this.seed = Math.imul(48271, this.seed))) / 2 ** 31 + ); + } + + public choose(list: Array): T { + return list[this.randomInInterval(0, list.length)]; + } + + public randomInInterval(aClosed: number, bOpen: number): number { + return Math.floor((bOpen - aClosed) * this.next) + aClosed; + } +} diff --git a/src/framework/helper/range.ts b/src/framework/helper/range.ts new file mode 100644 index 0000000..09cb2c7 --- /dev/null +++ b/src/framework/helper/range.ts @@ -0,0 +1,15 @@ +export const range = ({ + from = 0, + to = Infinity, + step = 1, +}: { + from?: number; + to?: number; + step?: number; +}): Iterable => { + return { + *[Symbol.iterator]() { + for (let i = from; i < to; yield i, i += step) {} + }, + }; +}; diff --git a/src/framework/helper/sum.ts b/src/framework/helper/sum.ts new file mode 100644 index 0000000..3fbd934 --- /dev/null +++ b/src/framework/helper/sum.ts @@ -0,0 +1,2 @@ +export const sum = (list: ArrayLike): number => + Array.prototype.reduce.call(list, (a, sum) => a + sum, 0); diff --git a/src/index.html b/src/index.html index c746dca..d8367a6 100644 --- a/src/index.html +++ b/src/index.html @@ -18,9 +18,7 @@
- +
diff --git a/src/page/about/about.scss b/src/page/about/about.scss index d0a7926..e49d989 100644 --- a/src/page/about/about.scss +++ b/src/page/about/about.scss @@ -1,5 +1,5 @@ -@import "../../style/mixins"; -@import "../../style/vars"; +@import '../../style/mixins'; +@import '../../style/vars'; #about { @include important-card(); diff --git a/src/page/about/about.ts b/src/page/about/about.ts index 341040b..11d6a5f 100644 --- a/src/page/about/about.ts +++ b/src/page/about/about.ts @@ -1,9 +1,9 @@ -import { PageContent } from "../content/content"; -import { Header } from "../../model/portfolio"; -import { PageElement } from "../../framework/page-element"; +import { PageContent } from '../content/content'; +import { Header } from '../../model/portfolio'; +import { PageElement } from '../../framework/page-element'; -import { generate } from "./about.html"; -import { createElement } from "../../framework/helper"; +import { generate } from './about.html'; +import { createElement } from '../../framework/helper/create-element'; export class PageHeader extends PageElement { public constructor(header: Header) { diff --git a/src/page/background/animation.ts b/src/page/background/animation.ts index cecb122..451e2cb 100644 --- a/src/page/background/animation.ts +++ b/src/page/background/animation.ts @@ -1,4 +1,4 @@ -import { Vec2 } from "./vec2"; +import { Vec2 } from './vec2'; export class Animation { private _value: Vec2; diff --git a/src/page/background/background.html.ts b/src/page/background/background.html.ts index a4ac278..5e1fc8d 100644 --- a/src/page/background/background.html.ts +++ b/src/page/background/background.html.ts @@ -2,5 +2,7 @@ import { html } from "../../model/misc"; import "./background.scss"; export const generate = (): html => ` - +
+
+
`; diff --git a/src/page/background/background.scss b/src/page/background/background.scss index 65d97eb..0f57011 100644 --- a/src/page/background/background.scss +++ b/src/page/background/background.scss @@ -1,11 +1,44 @@ @import "../../style/vars"; @import "../../style/mixins"; -canvas#background { +#background-container { position: fixed; - top: 0; left: 0; + top: 0; height: 100vh; width: 100%; - z-index: -10; + + z-index: -1; + -webkit-overflow-scrolling: touch; + perspective: 5px; + perspective-origin: center center; + overflow: hidden; + + #background { + overflow: hidden; + will-change: width, height; + transition: height $long-transition-time, width $long-transition-time; + transform-style: flat; + + div { + position: -webkit-sticky; + position: absolute; + left: 0; + top: 0; + border-radius: 100px; + width: 140px; + + transition: transform $long-transition-time, opacity $long-transition-time; + will-change: transform, opacity; + animation: fade-in 1s linear; + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + } + } } diff --git a/src/page/background/background.ts b/src/page/background/background.ts index c9d7be4..49776a6 100644 --- a/src/page/background/background.ts +++ b/src/page/background/background.ts @@ -1,31 +1,20 @@ -import { PageElement } from "../../framework/page-element"; -import { getHeight, createElement, sum } from "../../framework/helper"; -import { PageEvent, PageEventType } from "../../framework/page-event"; -import { Blob } from "./blob"; -import { generate } from "./background.html"; -import { Animation } from "./animation"; -import { Vec3 } from "./vec3"; -import { Vec2 } from "./vec2"; +import { PageElement } from '../../framework/page-element'; +import { PageEvent, PageEventType } from '../../framework/page-event'; +import { createElement } from '../../framework/helper/create-element'; +import { Blob } from './blob'; +import { generate } from './background.html'; +import { Random } from '../../framework/helper/random'; +import { getHeight } from '../../framework/helper/get-height'; +import { sum } from '../../framework/helper/sum'; export class PageBackground extends PageElement { private readonly blobs: Array = []; - private readonly blobSpacing = 140; - private readonly perspective = 5; - private readonly zMin = 10; - private readonly zMax = 30; - private readonly animationTime = 350; - private backgroundSize: Animation; - private scrollPosition: number = 0; - private previousTimestamp: DOMHighResTimeStamp = null; - private readonly canvas: HTMLCanvasElement; - private readonly ctx: CanvasRenderingContext2D; + private readonly blobSpacing = 350; public constructor(private start: PageElement, private end: PageElement) { super(); - this.canvas = createElement(generate()) as HTMLCanvasElement; - this.ctx = this.canvas.getContext("2d"); - this.setElement(this.canvas); - Blob.initialize(this.zMin, this.zMax); + this.setElement(createElement(generate())); + Blob.initialize(10, 30, 5); } protected handleEvent(event: PageEvent, parent: PageElement) { @@ -37,49 +26,55 @@ export class PageBackground extends PageElement { } private bindListeners(parent: PageElement) { - window.addEventListener("resize", () => this.resize(parent)); - window.addEventListener("load", e => { - this.resize(parent); - this.redraw(e.timeStamp, parent); - }); + window.addEventListener('resize', () => this.resize(parent)); + window.addEventListener('load', () => this.resize(parent)); + parent + .getElement() + .addEventListener( + 'scroll', + () => (this.getElement().scrollTop = parent.getElement().scrollTop) + ); } private resize(parent: PageElement, heightChange?: number) { - this.resizeCanvas(); - this.resizeBackground(parent, heightChange); - } - - private resizeCanvas() { - this.canvas.width = this.canvas.clientWidth; - this.canvas.height = this.canvas.clientHeight; - } - - private resizeBackground(parent: PageElement, heightChange?: number) { - const targetWidth = parent.getElement().clientWidth; - const siblings: Array = this.getSiblings(parent); - let targetHeight = sum(siblings.map(getHeight)); + + const width = parent.getElement().clientWidth; + let height = sum(siblings.map(getHeight)); if (heightChange) { - targetHeight += heightChange; + height += heightChange; } - const targetSize = new Vec2(targetWidth, targetHeight); + this.query('#background').style.width = `${width}px`; + this.query('#background').style.height = `${height}px`; - this.backgroundSize = new Animation( - this.backgroundSize ? this.backgroundSize.value : targetSize, - targetSize, - this.animationTime, - backgroundSize => - this.blobs.forEach(blob => { - const topLeft = this.convertFrom2Dto3D(Vec2.Zero, blob.z); - const bottomRight = this.convertFrom2Dto3D( - backgroundSize, - blob.z, - backgroundSize.y - this.canvas.height - ); - blob.positionScale = bottomRight.subtract(topLeft); - }) + const requiredBlobCount = Math.round( + (width * height) / this.blobSpacing ** 2 ); + + while (requiredBlobCount > this.blobs.length) { + const blob = new Blob(); + this.query('#background').appendChild(blob.htmlElement); + this.blobs.push(blob); + } + + const random = new Random(2662); + + this.blobs.forEach((b, i) => { + if (i >= requiredBlobCount) { + b.hide(); + } else { + b.transform( + random, + width, + parent.getElement().clientHeight, + height, + getHeight(this.start.getElement()), + getHeight(this.end.getElement()) + ); + b.show(); + } + }); } private getSiblings(parent: PageElement): Array { @@ -87,77 +82,4 @@ export class PageBackground extends PageElement { .call(parent.getElement().children) .filter(e => e !== this.getElement()); } - - private redraw(timestamp: DOMHighResTimeStamp, parent: PageElement) { - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - const deltaTime = this.getDeltaTime(timestamp); - this.backgroundSize.step(deltaTime); - this.scrollPosition = parent.getElement().scrollTop; - const requiredBlobCount = this.requiredBlobCount; - - while (requiredBlobCount > this.blobs.length) { - this.blobs.push(new Blob()); - } - - this.blobs.sort((b1, b2) => b2.z - b1.z); - - this.blobs.forEach((blob, i) => { - if (i >= requiredBlobCount) { - return; - } - - const topLeft = this.convertFrom3Dto2D(blob.topLeft); - const bottomRight = this.convertFrom3Dto2D( - blob.topLeft.add(Vec3.from(blob.size, 0)) - ); - - if (this.isInView(topLeft) || this.isInView(bottomRight)) { - blob.draw(this.ctx, topLeft, bottomRight.subtract(topLeft)); - } - }); - - window.requestAnimationFrame(timestamp => this.redraw(timestamp, parent)); - } - - 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 = this.perspective / (this.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: number = 0 - ): Vec2 { - const m = 1 + z / this.perspective; - return new Vec2(p.x * m - z / 2, p.y * m - z / 2 + scrollPosition); - } - - private isInView(p: Vec2): boolean { - return ( - 0 <= p.x && - p.x <= this.canvas.width && - 0 <= p.y && - p.y <= this.canvas.height - ); - } - - private get requiredBlobCount(): number { - return Math.round( - (this.backgroundSize.value.x * this.backgroundSize.value.y) / - this.blobSpacing ** 2 - ); - } } diff --git a/src/page/background/blob.ts b/src/page/background/blob.ts index 902390f..f4e1e8a 100644 --- a/src/page/background/blob.ts +++ b/src/page/background/blob.ts @@ -1,74 +1,96 @@ -import { - choose, - mixColors, - randomFactory, - randomInInterval -} from "../../framework/helper"; -import { Vec2 } from "./vec2"; -import { Vec3 } from "./vec3"; +import { mixColors } from '../../framework/helper/color-mixer'; +import { createElement } from '../../framework/helper/create-element'; +import { Random } from '../../framework/helper/random'; export class Blob { - private static readonly creatorRandom = randomFactory(44); - private static readonly colors = ["#fff9e0", "#ffd6d6"]; - private static readonly rotation = (-20 / 180) * Math.PI; - + private static readonly creatorRandom = new Random(44); + private static readonly colors = ['#fff9e0', '#ffd6d6']; private static zMin: number; private static zMax: number; - public static initialize(zMin: number, zMax: number) { + private static perspective: number; + public static initialize(zMin: number, zMax: number, perspective: number) { Blob.zMin = zMin; Blob.zMax = zMax; + Blob.perspective = perspective; } - public readonly z = randomInInterval( + private readonly z = Blob.creatorRandom.randomInInterval( Blob.zMin, - Blob.zMax, - Blob.creatorRandom + Blob.zMax ); - private readonly positionQ = new Vec2( - Blob.creatorRandom(), - Blob.creatorRandom() - ); - private _positionScale = new Vec2(0, 0); - - private readonly _size = new Vec2( - 140, - randomInInterval(160, 740, Blob.creatorRandom) - ); - private readonly color = - "#" + - mixColors( - "#ffffff", - choose(Blob.colors, Blob.creatorRandom), + private readonly element: HTMLElement = createElement('
'); + constructor() { + this.element.style.backgroundColor = mixColors( + '#ffffff', + Blob.creatorRandom.choose(Blob.colors), (this.z - Blob.zMin) / (Blob.zMax - Blob.zMin) ); - - public get topLeft(): Vec3 { - return Vec3.from(this.positionQ.multiply(this._positionScale), this.z); + this.element.style.zIndex = (-this.z).toString(); + this.element.style.height = `${Blob.creatorRandom.randomInInterval( + 160, + 740 + )}px`; } - public get size(): Vec2 { - return this._size; + get htmlElement(): HTMLElement { + return this.element; } - public set positionScale(value: Vec2) { - this._positionScale = value; + private randomWithKnownZ( + random: Random, + viewportSize: number, + scrollSize: number, + startOffset = 0, + endOffset = 0 + ): number { + const m = 1 + this.z / Blob.perspective; + + const variableOffset = (offset, q) => + Math.max( + 0, + offset - ((this.z - Blob.zMin) / (Blob.zMax - Blob.zMin)) * (offset * q) + ); + + startOffset = variableOffset(startOffset, 1); + endOffset = variableOffset(endOffset, 0.2); + + const lowerBound = viewportSize / 2 - (viewportSize / 2 - startOffset) * m; + const l = + scrollSize - viewportSize + (viewportSize - startOffset - endOffset) * m; + + return random.randomInInterval(lowerBound, lowerBound + l); } - public draw(ctx: CanvasRenderingContext2D, position: Vec2, size: Vec2) { - ctx.save(); + public show() { + this.element.style.opacity = '1'; + } - ctx.translate(position.x, position.y); - ctx.rotate(Blob.rotation); + public hide() { + this.element.style.opacity = '0'; + } - ctx.beginPath(); - ctx.arc(0, size.x / 2 - size.y / 2, size.x / 2, Math.PI, 0); - ctx.arc(0, size.y / 2 - size.x / 2, size.x / 2, 0, Math.PI); - ctx.closePath(); - - ctx.fillStyle = this.color; - ctx.fill(); - - ctx.restore(); + public transform( + random: Random, + width: number, + viewportHeight: number, + scrollHeight: number, + startOffset: number, + endOffset: number + ) { + const value = ` + translateX(${this.randomWithKnownZ(random, width, width)}px) + translateY(${this.randomWithKnownZ( + random, + viewportHeight, + scrollHeight, + startOffset, + endOffset + )}px) + translateZ(${-this.z}px) + rotate(-20deg) + `; + this.element.style['-webkit-transform'] = value; + this.element.style.transform = value; } } diff --git a/src/page/background/vec3.ts b/src/page/background/vec3.ts index 3eda035..00af336 100644 --- a/src/page/background/vec3.ts +++ b/src/page/background/vec3.ts @@ -1,4 +1,4 @@ -import { Vec2 } from "./vec2"; +import { Vec2 } from './vec2'; export class Vec3 { public static readonly Zero = new Vec3(0, 0, 0); diff --git a/src/page/content/content.scss b/src/page/content/content.scss index 3b92ed1..f713ae1 100644 --- a/src/page/content/content.scss +++ b/src/page/content/content.scss @@ -1,4 +1,4 @@ -@import "../../style/vars"; +@import '../../style/vars'; .content { margin-top: $small-margin; diff --git a/src/page/content/content.ts b/src/page/content/content.ts index 716f8e5..be21302 100644 --- a/src/page/content/content.ts +++ b/src/page/content/content.ts @@ -1,8 +1,9 @@ -import { Content, TypedContent } from "../../model/content"; -import "./content.scss"; -import { PageElement } from "../../framework/page-element"; -import { createElement, last } from "../../framework/helper"; -import { html } from "../../model/misc"; +import { Content, TypedContent } from '../../model/content'; +import './content.scss'; +import { PageElement } from '../../framework/page-element'; +import { createElement } from '../../framework/helper/create-element'; +import { html } from '../../model/misc'; +import { last } from '../../framework/helper/last'; export class PageContent extends PageElement { private static isTyped(content): content is TypedContent { @@ -13,10 +14,10 @@ export class PageContent extends PageElement { element: TypedContent, disableInnerShadow?: boolean ): html { - if (element.type === "a") { + if (element.type === 'a') { return ` ${element.text} `; } - if (element.type === "video") { + if (element.type === 'video') { return ` `; } - if (element.type === "img") { + if (element.type === 'img') { return ` - ${!disableInnerShadow ? `
` : ""} + ${!disableInnerShadow ? `
` : ''} ${element.alt} - ${!disableInnerShadow ? `
` : ""} + ${!disableInnerShadow ? `
` : ''} `; } - throw new Error("Unhandled type."); + throw new Error('Unhandled type.'); } public constructor(content: Content) { @@ -51,7 +52,7 @@ export class PageContent extends PageElement { ? PageContent.parseTypedContent(element) : `

${element}

` ) - .join("\n")} + .join('\n')} `) ); diff --git a/src/page/footer/footer.scss b/src/page/footer/footer.scss index 109ff63..1db12b9 100644 --- a/src/page/footer/footer.scss +++ b/src/page/footer/footer.scss @@ -1,5 +1,5 @@ -@import "../../style/mixins"; -@import "../../style/vars"; +@import '../../style/mixins'; +@import '../../style/vars'; footer#page-footer { text-align: center; diff --git a/src/page/footer/footer.ts b/src/page/footer/footer.ts index 789c31a..c13ba16 100644 --- a/src/page/footer/footer.ts +++ b/src/page/footer/footer.ts @@ -1,8 +1,8 @@ -import { Footer } from "../../model/portfolio"; -import { PageElement } from "../../framework/page-element"; +import { Footer } from '../../model/portfolio'; +import { PageElement } from '../../framework/page-element'; -import { generate } from "./footer.html"; -import { createElement } from "../../framework/helper"; +import { generate } from './footer.html'; +import { createElement } from '../../framework/helper/create-element'; export class PageFooter extends PageElement { constructor(footer: Footer) { diff --git a/src/page/image-viewer/image-viewer.scss b/src/page/image-viewer/image-viewer.scss index eb5524b..da86d99 100644 --- a/src/page/image-viewer/image-viewer.scss +++ b/src/page/image-viewer/image-viewer.scss @@ -1,5 +1,5 @@ -@import "../../style/vars"; -@import "../../style/mixins"; +@import '../../style/vars'; +@import '../../style/mixins'; #image-viewer { @include center-children(); diff --git a/src/page/image-viewer/image-viewer.ts b/src/page/image-viewer/image-viewer.ts index e48298d..937ec7c 100644 --- a/src/page/image-viewer/image-viewer.ts +++ b/src/page/image-viewer/image-viewer.ts @@ -1,8 +1,8 @@ -import { PageElement } from "../../framework/page-element"; +import { PageElement } from '../../framework/page-element'; -import { generate } from "./image-viewer.html"; -import { createElement } from "../../framework/helper"; -import { PageEvent, PageEventType } from "../../framework/page-event"; +import { generate } from './image-viewer.html'; +import { PageEvent, PageEventType } from '../../framework/page-event'; +import { createElement } from '../../framework/helper/create-element'; export class PageImageViewer extends PageElement { public constructor() { @@ -17,16 +17,16 @@ export class PageImageViewer extends PageElement { return; } - document.body.addEventListener("keydown", this.handleKeydown.bind(this)); + document.body.addEventListener('keydown', this.handleKeydown.bind(this)); const images = Array.prototype.slice.call( - parent.getElement().querySelectorAll("img") + parent.getElement().querySelectorAll('img') ); images .filter( (img: HTMLImageElement) => img.parentElement !== this.getElement() && - !img.classList.contains("no-open") + !img.classList.contains('no-open') ) .forEach( (img: HTMLImageElement) => (img.onclick = this.handleClick.bind(this)) @@ -35,22 +35,22 @@ export class PageImageViewer extends PageElement { private handleClick(event: Event) { (this.query( - "#photo" + '#photo' ) as HTMLImageElement).src = (event.target as HTMLImageElement).src; PageImageViewer.show(this.getElement()); } private handleKeydown(event: KeyboardEvent) { - if (event.key === "Escape") { + if (event.key === 'Escape') { PageImageViewer.hide(this.getElement()); } } private static show(e: HTMLElement) { - e.style.display = "flex"; + e.style.display = 'flex'; } private static hide(e: HTMLElement) { - e.style.display = "none"; + e.style.display = 'none'; } } diff --git a/src/page/timeline/timeline-element/timeline-element.scss b/src/page/timeline/timeline-element/timeline-element.scss index 72c75af..36d9afa 100644 --- a/src/page/timeline/timeline-element/timeline-element.scss +++ b/src/page/timeline/timeline-element/timeline-element.scss @@ -1,5 +1,5 @@ -@import "../../../style/mixins"; -@import "../../../style/vars"; +@import '../../../style/mixins'; +@import '../../../style/vars'; .timeline-element { display: flex; @@ -9,7 +9,7 @@ border-left: $line-width solid $accent-color; &:before { - content: ""; + content: ''; @include square($icon-size); position: absolute; left: calc(-0.5 * #{$icon-size} - (1.5 * #{$line-width})); diff --git a/src/page/timeline/timeline-element/timeline-element.ts b/src/page/timeline/timeline-element/timeline-element.ts index e4fbe06..0cca8b4 100644 --- a/src/page/timeline/timeline-element/timeline-element.ts +++ b/src/page/timeline/timeline-element/timeline-element.ts @@ -1,9 +1,9 @@ -import { TimelineElement } from "../../../model/portfolio"; -import { PageContent } from "../../content/content"; -import { PageElement } from "../../../framework/page-element"; -import { generate } from "./timeline-element.html"; -import { createElement } from "../../../framework/helper"; -import { PageEventType } from "../../../framework/page-event"; +import { TimelineElement } from '../../../model/portfolio'; +import { PageContent } from '../../content/content'; +import { PageElement } from '../../../framework/page-element'; +import { generate } from './timeline-element.html'; +import { PageEventType } from '../../../framework/page-event'; +import { createElement } from '../../../framework/helper/create-element'; export class PageTimelineElement extends PageElement { private isOpen; @@ -20,19 +20,19 @@ export class PageTimelineElement extends PageElement { const content = new PageContent(timelineElement.more); super([content]); this.isOpen = false; - this.more = root.querySelector(".more"); + this.more = root.querySelector('.more'); this.more.appendChild(content.getElement()); - window.addEventListener("resize", this.handleResize.bind(this)); + window.addEventListener('resize', this.handleResize.bind(this)); root - .querySelector(".buttons") - .addEventListener("click", this.toggleOpen.bind(this)); + .querySelector('.buttons') + .addEventListener('click', this.toggleOpen.bind(this)); } else super(); this.setElement(root); } private toggleOpen() { - const showMore = this.query(".show-more") as HTMLElement; - const showLess = this.query(".show-less") as HTMLElement; + const showMore = this.query('.show-more') as HTMLElement; + const showLess = this.query('.show-less') as HTMLElement; if (this.isOpen) { PageTimelineElement.show(showMore); PageTimelineElement.hide(showLess); @@ -49,28 +49,28 @@ export class PageTimelineElement extends PageElement { private notifyOfHeightChange(deltaHeight: number = undefined) { this.eventBroadcaster?.broadcastEvent({ type: PageEventType.onBodyDimensionsChanged, - data: { deltaHeight } + data: { deltaHeight }, }); setTimeout( () => this.eventBroadcaster?.broadcastEvent({ - type: PageEventType.onBodyDimensionsChanged + type: PageEventType.onBodyDimensionsChanged, }), 350 ); } private static hide(element: HTMLElement) { - element.style.opacity = "0"; + element.style.opacity = '0'; setTimeout(() => { - element.style.visibility = "hidden"; + element.style.visibility = 'hidden'; }, 350); } private static show(element: HTMLElement) { - element.style.visibility = "visible"; - element.style.opacity = "1"; + element.style.visibility = 'visible'; + element.style.opacity = '1'; } private openMore() { @@ -81,13 +81,13 @@ export class PageTimelineElement extends PageElement { private closeMore() { const deltaHeight = this.more.scrollHeight; - this.more.style.height = "0"; + this.more.style.height = '0'; this.notifyOfHeightChange(-deltaHeight); } private handleResize() { if (this.isOpen) { - this.more.style.height = "auto"; + this.more.style.height = 'auto'; setTimeout(this.openMore.bind(this), 200); } } diff --git a/src/page/timeline/timeline.scss b/src/page/timeline/timeline.scss index 0cf23da..9fc157e 100644 --- a/src/page/timeline/timeline.scss +++ b/src/page/timeline/timeline.scss @@ -1,4 +1,4 @@ -@import "../../style/vars"; +@import '../../style/vars'; #timeline { width: $body-width; diff --git a/src/page/timeline/timeline.ts b/src/page/timeline/timeline.ts index 1b9594d..e688697 100644 --- a/src/page/timeline/timeline.ts +++ b/src/page/timeline/timeline.ts @@ -1,8 +1,8 @@ -import { TimelineElement } from "../../model/portfolio"; -import { PageElement } from "../../framework/page-element"; -import { PageTimelineElement } from "./timeline-element/timeline-element"; -import { generate } from "./timeline.html"; -import { createElement } from "../../framework/helper"; +import { TimelineElement } from '../../model/portfolio'; +import { PageElement } from '../../framework/page-element'; +import { PageTimelineElement } from './timeline-element/timeline-element'; +import { generate } from './timeline.html'; +import { createElement } from '../../framework/helper/create-element'; export class PageTimeline extends PageElement { public constructor( diff --git a/src/portfolio.ts b/src/portfolio.ts index 6bdbf00..42699c7 100644 --- a/src/portfolio.ts +++ b/src/portfolio.ts @@ -1,39 +1,39 @@ -import { Portfolio } from "./model/portfolio"; +import { Portfolio } from './model/portfolio'; -import me from "./static/media/me.jpg"; -import forexMP4 from "./static/media/forex.mp4"; -import forexWEBM from "./static/media/forex.webm"; -import myNotes from "./static/media/my-notes.jpg"; -import processSimulator from "./static/media/process-simulator.jpg"; -import processSimulatorInput from "./static/media/process-simulator-input.jpg"; -import citySimulation from "./static/media/simulation.jpg"; -import color from "./static/media/color.jpg"; -import platform from "./static/media/platform.png"; -import photos from "./static/media/photos.jpg"; -import led from "./static/media/led.jpg"; -import cv from "./static/cv/andras_schmelczer_cv_2020_01.pdf"; -import ledMP4 from "./static/media/led.mp4"; -import ledWEBM from "./static/media/led.webm"; +import me from './static/media/me.jpg'; +import forexMP4 from './static/media/forex.mp4'; +import forexWEBM from './static/media/forex.webm'; +import myNotes from './static/media/my-notes.jpg'; +import processSimulator from './static/media/process-simulator.jpg'; +import processSimulatorInput from './static/media/process-simulator-input.jpg'; +import citySimulation from './static/media/simulation.jpg'; +import color from './static/media/color.jpg'; +import platform from './static/media/platform.png'; +import photos from './static/media/photos.jpg'; +import led from './static/media/led.jpg'; +import cv from './static/cv/andras_schmelczer_cv_2020_01.pdf'; +import ledMP4 from './static/media/led.mp4'; +import ledWEBM from './static/media/led.webm'; export const portfolio: Portfolio = { config: { showMore: `Show details`, - showLess: `Show less` + showLess: `Show less`, }, header: { name: `AndrĂ¡s Schmelczer`, picture: { type: `img`, image: me, - alt: `a picture of me` + alt: `a picture of me`, }, about: [ `I have always been fascinated by the engineering feats that surround us. When I realized that someday I might be able to contribute to these achievements, I knew that is what I need to aim for. As I am finishing my fifth semester at the Budapest University of Technology and Economics, I feel I am getting closer to it every day.`, - `You can see some of the more interesting projects I have worked on below.` - ] + `You can see some of the more interesting projects I have worked on below.`, + ], }, timeline: [ { @@ -43,7 +43,7 @@ export const portfolio: Portfolio = { type: `video`, options: `autoplay loop muted playsinline`, webm: forexWEBM, - mp4: forexMP4 + mp4: forexMP4, }, description: `From the animation we can see that my algorithm does a somewhat acceptable job at predicting (blue graph) the EUR/USD rates (green graph).`, @@ -53,8 +53,8 @@ export const portfolio: Portfolio = { 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.`, `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.` - ] + a mostly profitable trading strategy is viable. In my free time I may put more work into it.`, + ], }, { date: `2019 November`, @@ -62,19 +62,19 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: myNotes, - alt: `two screenshots of the application` + alt: `two screenshots of the application`, }, description: `A minimalist note organizer and editor powered by Markwon.`, more: [ { type: `a`, href: `https://github.com/schmelczerandras/my-notes`, - text: `MyNotes on GitHub` + text: `MyNotes on GitHub`, }, `A basic android app for creating and filtering notes written in markdown.`, `It was my homework for BME's Android and web development course. - It was also my first experience with Android development.` - ] + It was also my first experience with Android development.`, + ], }, { date: `2018 October - November`, @@ -82,7 +82,7 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: processSimulator, - alt: `a screenshot of the simulator` + alt: `a screenshot of the simulator`, }, description: `Dynamically calculating the temperatures and flow velocities in a fluid based cooling system based on a simple model.`, @@ -91,8 +91,8 @@ export const portfolio: Portfolio = { drains sources, and of course, pipes.`, `The algorithm takes advantages of graphs and matrices to get to a next time frame.`, `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.` - ] + the communication between the layers. For drawing the frontend HTML5 canvas is utilized.`, + ], }, { date: `2018 October - November`, @@ -100,15 +100,15 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: processSimulatorInput, - alt: `a picture of the simulator's UI` + alt: `a picture of the simulator's UI`, }, description: `An intuitive editor to create and edit input files for the nuclear facility simulator.`, more: [ `Nodes can be moved with drag&drop gestures. Editing the parameters of elements can be done on the right panel.`, `The UI is built with JavaFX. The output can be exported as JSON or - directly uploaded to the simulation backend.` - ] + directly uploaded to the simulation backend.`, + ], }, { date: `2018 July - August`, @@ -116,7 +116,7 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: citySimulation, - alt: `a picture of a low-poly city` + alt: `a picture of a low-poly city`, }, description: `Simulating a city where car crashes are more frequent than usual.`, more: [ @@ -129,8 +129,8 @@ export const portfolio: Portfolio = { The decisions of the agents is calculated server-side. The real challenge was broadcasting these decisions in a fault-tolerant way using minimal bandwidth.`, `The program is made with Unity using C# as the scripting language. The models and animations - were also made by me using Blender.` - ] + were also made by me using Blender.`, + ], }, { date: `2018 June`, @@ -138,7 +138,7 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: color, - alt: `a picture of the app` + alt: `a picture of the app`, }, description: `An innovative (at least I thought so) color grader web application.`, more: [ @@ -149,8 +149,8 @@ export const portfolio: Portfolio = { function of their distance to the selected color.`, `By clicking on a colored 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).`, - { type: `a`, href: `color`, text: `schmelczer.dev/color` } - ] + { type: `a`, href: `color`, text: `schmelczer.dev/color` }, + ], }, { date: `2017 autumn`, @@ -159,14 +159,14 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: platform, - alt: `a picture of the app` + alt: `a picture of the app`, }, description: `A 3D game written in C with the help of SDL 1.2 (I haven't heard of GPU programming at the time).`, more: [ `The maps are randomly generated and fully destroyable. The player is getting chased by flying enemies. Overall, I find it a really enjoyable game.`, - `I did this as a homework for my Basics of Programming course.` - ] + `I did this as a homework for my Basics of Programming course.`, + ], }, { date: `2016 summer`, @@ -174,10 +174,10 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: photos, - alt: `a picture of the website` + alt: `a picture of the website`, }, description: `A simple web page where you can view my photos.`, - link: { type: `a`, href: `photos`, text: `schmelczer.dev/photos` } + link: { type: `a`, href: `photos`, text: `schmelczer.dev/photos` }, }, { date: `2016 spring`, @@ -185,7 +185,7 @@ export const portfolio: Portfolio = { figure: { type: `img`, image: led, - alt: `a picture from the video` + alt: `a picture from the video`, }, description: `A full stack application with a built-in music player which music controls the color of some RGB LED strips.`, @@ -196,9 +196,9 @@ export const portfolio: Portfolio = { A quite simple frontend for accessing the music player and changing the settings also got built using vanilla web development technologies.`, `Below is a video showing the system in work.`, - { type: `video`, mp4: ledMP4, webm: ledWEBM, options: "controls" } - ] - } + { type: `video`, mp4: ledMP4, webm: ledWEBM, options: 'controls' }, + ], + }, ], footer: { title: `Learn more`, @@ -206,6 +206,6 @@ export const portfolio: Portfolio = { email: `schmelczerandras@gmail.com`, cvName: `Curriculum vitae`, lastEditName: `Last modified on `, - lastEdit: new Date(2020, 0, 2) // months are 0 indexed - } + lastEdit: new Date(2020, 0, 2), // months are 0 indexed + }, }; diff --git a/src/style/a.scss b/src/style/a.scss index 3d3bc7a..65da6ed 100644 --- a/src/style/a.scss +++ b/src/style/a.scss @@ -1,5 +1,5 @@ -@import "vars"; -@import "mixins"; +@import 'vars'; +@import 'mixins'; a { @include insignificant-font(); @@ -13,14 +13,13 @@ a { $border-shift: 10px; transition: transform $long-transition-time; - $dot-size: 4px; &:before { - content: ""; + content: ''; display: block; position: absolute; width: 100%; - height: $dot-size; + height: $line-width; bottom: 0; z-index: 1; background: linear-gradient( @@ -33,11 +32,11 @@ a { } &:after { - content: ""; + content: ''; display: block; width: calc(100% + #{$border-shift}); z-index: 0; - border-bottom: $dot-size dotted $accent-color; + border-bottom: $line-width dashed $accent-color; transition: transform $long-transition-time; } diff --git a/src/style/mixins.scss b/src/style/mixins.scss index 07b558d..c4f4290 100644 --- a/src/style/mixins.scss +++ b/src/style/mixins.scss @@ -1,4 +1,4 @@ -@import "vars"; +@import 'vars'; @mixin center-children() { display: flex; @@ -47,7 +47,7 @@ } @mixin title-font() { - font: 400 3.5rem "Montserrat", serif; + font: 400 3.5rem 'Montserrat', serif; font-style: normal; line-height: 1; @@ -58,16 +58,16 @@ } @mixin sub-title-font() { - font: 400 2rem "Montserrat", serif; + font: 400 2rem 'Montserrat', serif; font-style: normal; } @mixin main-font() { - font: 400 1.25rem "Lato", sans-serif; + font: 400 1.25rem 'Lato', sans-serif; line-height: 1.6; } @mixin insignificant-font() { - font: 400 1.1rem "Lato", sans-serif; + font: 400 1.1rem 'Lato', sans-serif; font-style: italic; } diff --git a/src/style/vars.scss b/src/style/vars.scss index 248b10f..eb2f3eb 100644 --- a/src/style/vars.scss +++ b/src/style/vars.scss @@ -1,4 +1,4 @@ -@import "fonts"; +@import 'fonts'; $background: white; @@ -13,7 +13,7 @@ $scrollbar-color: #ffd6d6; $short-transition-time: 220ms; $long-transition-time: 350ms; $line-width: 3px; -$border-radius: 15px; +$border-radius: var(--border-radius); $breakpoint-width: 925px; $large-margin: var(--large-margin); @@ -36,6 +36,7 @@ $body-width: var(--body-width); --body-width: 765px; --shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.075); --shadow2: 0 0 1px rgba(0, 0, 0, 0.2); + --border-radius: 15px; @media (max-width: $breakpoint-width) { --large-margin: 60px; @@ -45,5 +46,6 @@ $body-width: var(--body-width); --body-width: 90%; --shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.05); --shadow2: 0 0 1px rgba(0, 0, 0, 0.125); + --border-radius: 10px; } } diff --git a/src/styles.scss b/src/styles.scss index fc20af0..ac04083 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,6 +1,6 @@ -@import "style/vars"; -@import "style/mixins"; -@import "style/a"; +@import 'style/vars'; +@import 'style/mixins'; +@import 'style/a'; * { margin: 0; diff --git a/webpack.config.js b/webpack.config.js index e98696f..1ceacd9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,157 +1,158 @@ -const path = require("path"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const { CleanWebpackPlugin } = require("clean-webpack-plugin"); -const TerserJSPlugin = require("terser-webpack-plugin"); -const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin"); +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const TerserJSPlugin = require('terser-webpack-plugin'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); -const isProduction = process.env.NODE_ENV === "production"; +const isProduction = process.env.NODE_ENV === 'production'; module.exports = { watchOptions: { - ignored: /node_modules/ + ignored: /node_modules/, }, devServer: { - host: "0.0.0.0" + host: '0.0.0.0', + // disableHostCheck: true }, optimization: { - minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})] + minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ xhtml: true, minify: true, - template: "./src/index.html", - inlineSource: ".(js|css)$" + template: './src/index.html', + inlineSource: '.(js|css)$', }), new HtmlWebpackInlineSourcePlugin(), new MiniCssExtractPlugin({ - filename: "[name].[contenthash].css", - chunkFilename: "[id].[contenthash].css" - }) + filename: '[name].[contenthash].css', + chunkFilename: '[id].[contenthash].css', + }), ], entry: { - index: "./src/index.ts" + index: './src/index.ts', }, module: { rules: [ { test: /\.(jpe?g|png)$/i, - loader: "responsive-loader", + loader: 'responsive-loader', options: { - adapter: require("responsive-loader/sharp"), - outputPath: "static/", + adapter: require('responsive-loader/sharp'), + outputPath: 'static/', sizes: [300, 600, 1200, 2000], - placeholder: false - } + placeholder: false, + }, }, { test: /\.(webm|mp4)$/i, use: [ { - loader: "file-loader", + loader: 'file-loader', query: { - outputPath: "static/" - } + outputPath: 'static/', + }, }, { - loader: "image-webpack-loader", + loader: 'image-webpack-loader', options: { disable: !isProduction, mozjpeg: { progressive: true, - quality: 65 + quality: 65, }, optipng: { - enabled: true + enabled: true, }, pngquant: { quality: [0.65, 0.9], - speed: 4 + speed: 4, }, gifsicle: { - interlaced: false + interlaced: false, }, webp: { - quality: 65 - } - } - } - ] + quality: 65, + }, + }, + }, + ], }, { test: /\.svg$/, - loader: "svg-url-loader", + loader: 'svg-url-loader', options: { limit: 10 * 1024, - noquotes: true - } + noquotes: true, + }, }, { test: /\.(pdf)$/i, use: { - loader: "file-loader", + loader: 'file-loader', query: { - outputPath: "static/", - name: "[name].[ext]" - } - } + outputPath: 'static/', + name: '[name].[ext]', + }, + }, }, { test: /\.ico$/i, use: { - loader: "file-loader", + loader: 'file-loader', query: { - outputPath: "/", - name: "[name].[ext]" - } - } + outputPath: '/', + name: '[name].[ext]', + }, + }, }, { test: /\.scss$/i, use: [ MiniCssExtractPlugin.loader, - "css-loader", - "postcss-loader", + 'css-loader', + 'postcss-loader', { - loader: "resolve-url-loader", + loader: 'resolve-url-loader', options: { - keepQuery: true - } + keepQuery: true, + }, }, { - loader: "sass-loader", + loader: 'sass-loader', options: { - sourceMap: true - } - } - ] + sourceMap: true, + }, + }, + ], }, { test: /\.(woff2?|ttf|eot|svg)(?:[?#].+)?$/, use: { - loader: "file-loader", + loader: 'file-loader', options: { - name: "[name].[ext]", - outputPath: "static/fonts/" - } + name: '[name].[ext]', + outputPath: 'static/fonts/', + }, }, - include: /fonts/ + include: /fonts/, }, { test: /\.ts$/, - use: "ts-loader", - exclude: /node_modules/ - } - ] + use: 'ts-loader', + exclude: /node_modules/, + }, + ], }, resolve: { - extensions: [".ts", ".js"] + extensions: ['.ts', '.js'], }, output: { - filename: "[name].[contenthash].js", - path: path.resolve(__dirname, "dist") - } + filename: '[name].[contenthash].js', + path: path.resolve(__dirname, 'dist'), + }, };