From 1893b774e7aab4a094a1f828cfebdb17dc07bcca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Schmelczer=20Andr=C3=A1s?= Date: Sat, 11 Jan 2020 13:05:40 +0100 Subject: [PATCH] finally --- .idea/workspace.xml | 33 ++- src/index.html | 4 +- src/page/background/animation.ts | 32 +++ .../{blob.html.ts => background.html.ts} | 4 +- src/page/background/background.scss | 28 +-- src/page/background/background.ts | 188 ++++++++++++++---- src/page/background/blob.ts | 128 ++++++------ src/page/background/vec2.ts | 17 ++ src/page/background/vec3.ts | 23 +++ src/page/index.ts | 2 +- src/page/timeline/timeline.html.ts | 2 +- src/page/timeline/timeline.scss | 2 +- src/style/mixins.scss | 1 + src/styles.scss | 55 ++--- 14 files changed, 345 insertions(+), 174 deletions(-) create mode 100644 src/page/background/animation.ts rename src/page/background/{blob.html.ts => background.html.ts} (73%) create mode 100644 src/page/background/vec2.ts create mode 100644 src/page/background/vec3.ts diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e474666..f06bc1e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,11 +2,20 @@ + + + + + + + + + + @@ -127,6 +137,7 @@ + @@ -166,17 +177,25 @@ - + - - + + - - + + - + + + + + + + + + \ No newline at end of file diff --git a/src/index.html b/src/index.html index 8c7d5bd..0a59f17 100644 --- a/src/index.html +++ b/src/index.html @@ -17,6 +17,8 @@ Portfolio - AndrĂ¡s Schmelczer - +
+ +
diff --git a/src/page/background/animation.ts b/src/page/background/animation.ts new file mode 100644 index 0000000..6a6224c --- /dev/null +++ b/src/page/background/animation.ts @@ -0,0 +1,32 @@ +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 + ) {} + + 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/blob.html.ts b/src/page/background/background.html.ts similarity index 73% rename from src/page/background/blob.html.ts rename to src/page/background/background.html.ts index cebaae1..4475c9e 100644 --- a/src/page/background/blob.html.ts +++ b/src/page/background/background.html.ts @@ -1,6 +1,6 @@ -import { html } from '../../framework/model/misc'; import './background.scss'; +import { html } from '../../framework/model/misc'; export const generate = (): html => ` -
+ `; diff --git a/src/page/background/background.scss b/src/page/background/background.scss index 77a01b7..e349278 100644 --- a/src/page/background/background.scss +++ b/src/page/background/background.scss @@ -1,34 +1,18 @@ @use '../../style/include' as *; @include responsive() using ($vars) { - div.background-element { - position: absolute; + canvas#background { + position: fixed; top: 0; left: 0; - border-radius: 100px; - width: 140px; + height: 100%; + width: 100%; + z-index: -10; - overflow: visible !important; // IE11 fix for disappearing elements - - @media (prefers-reduced-motion: reduce) { + @media print { & { display: none; } } - - transition: transform map_get($vars, $transition-time), - opacity map_get($vars, $transition-time), - background-color map_get($vars, $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 63d6974..53c2106 100644 --- a/src/page/background/background.ts +++ b/src/page/background/background.ts @@ -1,21 +1,37 @@ import { PageElement } from '../../framework/page-element'; -import { PageEvent, PageEventType } from '../../framework/events/page-event'; import { Blob } from './blob'; -import { Random } from '../../framework/helper/random'; -import { getHeight } from '../../framework/helper/get-height'; +import { generate } from './background.html'; +import { Animation } from './animation'; +import { Vec3 } from './vec3'; +import { Vec2 } from './vec2'; +import { PageEvent, PageEventType } from '../../framework/events/page-event'; +import { createElement } from '../../framework/helper/create-element'; import { sum } from '../../framework/helper/sum'; +import { getHeight } from '../../framework/helper/get-height'; export class PageBackground extends PageElement { private readonly blobs: Array = []; - private readonly blobSpacing = 350; + private readonly blobSpacing = 325; + private readonly minBlobCount = 30; + private readonly perspective = 5; + private readonly zMin = 10; + private readonly zMax = 30; + private readonly animationTime = 250; + private backgroundSize: Animation; + private scrollPosition: number = 0; + private previousTimestamp: DOMHighResTimeStamp = null; + private readonly canvas: HTMLCanvasElement; + private readonly ctx: CanvasRenderingContext2D; public constructor( private readonly start: PageElement, private readonly inBetween: Array, private readonly end: PageElement ) { - super(); - Blob.initialize(10, 30, 5); + super(createElement(generate())); + this.canvas = this.element as HTMLCanvasElement; + this.ctx = this.canvas.getContext('2d'); + Blob.initialize(this.zMin, this.zMax); } protected handleEvent(event: PageEvent, parent: PageElement) { @@ -35,48 +51,146 @@ export class PageBackground extends PageElement { private bindListeners(parent: PageElement) { window.addEventListener('resize', () => this.resize(parent)); - window.addEventListener('load', () => this.resize(parent)); + window.addEventListener('load', e => { + this.resize(parent); + this.redraw(e.timeStamp, parent); + }); } 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.element.clientWidth; + const siblings: Array = this.getSiblings(); - - const width = document.body.clientWidth; - let height = sum(siblings.map(getHeight)); + let targetHeight = sum(siblings.map(getHeight)); if (heightChange) { - height += heightChange; + targetHeight += heightChange; } - const requiredBlobCount = Math.round( - (width * height) / this.blobSpacing ** 2 + const targetSize = new Vec2(targetWidth, targetHeight); + + this.backgroundSize = new Animation( + this.backgroundSize ? this.backgroundSize.value : targetSize, + targetSize, + this.animationTime, + (from: Vec2, to: Vec2, q: number): Vec2 => + new Vec2(from.x + q * (to.x - from.x), from.y + q * (to.y - from.y)), + backgroundSize => + this.blobs.forEach(blob => { + const variableOffset = (offset, q) => + Math.max( + 0, + offset - + ((blob.z - this.zMin) / (this.zMax - this.zMin)) * offset * q + ); + const topOffset = variableOffset(getHeight(this.start.element), 1); + const topLeft = this.convertFrom2Dto3D( + new Vec2(0, topOffset), + blob.z + ); + + const bottomOffset = variableOffset(getHeight(this.end.element), 0.2); + + const bottomRight = this.convertFrom2Dto3D( + new Vec2(this.canvas.width, this.canvas.height - bottomOffset), + blob.z, + backgroundSize.y - this.canvas.height + ); + + blob.positionOffset = topLeft; + blob.positionScale = bottomRight.subtract(topLeft); + }) ); - - while (requiredBlobCount > this.blobs.length) { - const blob = new Blob(); - parent.element.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.element.clientHeight, - height, - getHeight(this.start.element), - getHeight(this.end.element) - ); - b.show(); - } - }); } private getSiblings(): Array { return [this.start, ...this.inBetween, this.end].map(e => e.element); } + + 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.blobs.forEach(b => b.step(deltaTime)); + + this.scrollPosition = parent.element.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.max( + this.minBlobCount, + 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 9018d9c..ce9ddca 100644 --- a/src/page/background/blob.ts +++ b/src/page/background/blob.ts @@ -1,22 +1,22 @@ +import { Vec2 } from './vec2'; +import { Vec3 } from './vec3'; import { mixColors } from '../../framework/helper/mix-colors'; -import { createElement } from '../../framework/helper/create-element'; import { Random } from '../../framework/helper/random'; -import { generate } from './blob.html'; +import { Animation } from './animation'; export class Blob { - private static readonly creatorRandom = new Random(44); private static readonly darkColors = ['#2c477a']; private static readonly lightColors = ['#fff9e0', '#ffd6d6']; + private static readonly rotation = (-20 / 180) * Math.PI; + private static readonly creatorRandom = new Random(51); private static colorPickerRandom = new Random(132); private static isDarkThemed = false; private static zMin: number; private static zMax: number; - private static perspective: number; - public static initialize(zMin: number, zMax: number, perspective: number) { + public static initialize(zMin: number, zMax: number) { Blob.zMin = zMin; Blob.zMax = zMax; - Blob.perspective = perspective; } public static changeTheme(isDarkThemed: boolean) { @@ -24,89 +24,79 @@ export class Blob { Blob.isDarkThemed = isDarkThemed; } - private readonly z = Blob.creatorRandom.randomInInterval( - Blob.zMin, - Blob.zMax + public readonly z = Blob.creatorRandom.randomInInterval(Blob.zMin, Blob.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 readonly _size = new Vec2( + 140, + Blob.creatorRandom.randomInInterval(260, 740) ); - private readonly element: HTMLElement = createElement(generate()); - constructor() { + public constructor() { this.decideColor(); - this.element.style.zIndex = Math.round(-this.z).toString(); - this.element.style.height = `${Blob.creatorRandom.randomInInterval( - 160, - 740 - )}px`; } public decideColor() { - this.element.style.backgroundColor = mixColors( - Blob.isDarkThemed ? '#242638 ' : '#ffffff', + const target = mixColors( + Blob.isDarkThemed ? '#242638' : '#ffffff', Blob.colorPickerRandom.choose( Blob.isDarkThemed ? Blob.darkColors : Blob.lightColors ), (this.z - Blob.zMin) / (Blob.zMax - Blob.zMin) ); + + this.color = new Animation( + this.color ? this.color.value : target, + target, + 250, + (f, t, q) => mixColors(f, t, 1 - q) + ); } - get htmlElement(): HTMLElement { - return this.element; + public step(value) { + this.color?.step(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 get topLeft(): Vec3 { + return Vec3.from( + this.positionQ.multiply(this._positionScale).add(this._positionOffset), + this.z + ); } - public show() { - this.element.style.opacity = '1'; + public get size(): Vec2 { + return this._size; } - public hide() { - this.element.style.opacity = '0'; + public set positionScale(value: Vec2) { + this._positionScale = value; } - 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; + 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(Blob.rotation); + + 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(); + + ctx.fillStyle = this.color.value; + ctx.fill(); + + ctx.restore(); } } diff --git a/src/page/background/vec2.ts b/src/page/background/vec2.ts new file mode 100644 index 0000000..a7be2ac --- /dev/null +++ b/src/page/background/vec2.ts @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..00af336 --- /dev/null +++ b/src/page/background/vec3.ts @@ -0,0 +1,23 @@ +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/index.ts b/src/page/index.ts index 5ee76d8..92b30b6 100644 --- a/src/page/index.ts +++ b/src/page/index.ts @@ -11,7 +11,7 @@ export const create = ({ header, timeline, footer }: Portfolio) => { const pageTimeline = new PageTimeline(timeline); const pageFooter = new PageFooter(footer); - new ContainerPage(document.body, [ + new ContainerPage(document.body.querySelector('#main'), [ new PageImageViewer(), pageHeader, pageTimeline, diff --git a/src/page/timeline/timeline.html.ts b/src/page/timeline/timeline.html.ts index 00c86e7..8af4756 100644 --- a/src/page/timeline/timeline.html.ts +++ b/src/page/timeline/timeline.html.ts @@ -2,5 +2,5 @@ import { html } from '../../framework/model/misc'; import './timeline.scss'; export const generate = (): html => ` -
+
`; diff --git a/src/page/timeline/timeline.scss b/src/page/timeline/timeline.scss index 07b4a27..897e649 100644 --- a/src/page/timeline/timeline.scss +++ b/src/page/timeline/timeline.scss @@ -1,7 +1,7 @@ @use '../../style/include' as *; @include responsive() using ($vars) { - main#timeline { + div#timeline { @include on-large-screen { // workaround for IE & > :first-child { diff --git a/src/style/mixins.scss b/src/style/mixins.scss index 9cfe71c..9e1d985 100644 --- a/src/style/mixins.scss +++ b/src/style/mixins.scss @@ -19,6 +19,7 @@ padding: map_get($vars, vars.$normal-margin); box-shadow: map_get($vars, vars.$shadow); z-index: 1; + width: 100%; } @mixin square($size) { diff --git a/src/styles.scss b/src/styles.scss index 5ccdc7a..c371354 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -5,14 +5,9 @@ @include responsive() using ($vars) { & { height: 100%; - overflow: hidden; - - transform-style: preserve-3d; background-color: map_get($vars, $background); - transition: background-color map_get($vars, $transition-time); - - touch-action: manipulation; + transition: background-color linear map_get($vars, $transition-time); @include on-small-screen { font-size: 0.8rem; @@ -48,17 +43,13 @@ *::after { @include main-font($vars); - transform-style: preserve-3d; - margin: 0; padding: 0; box-sizing: border-box; - transition: background-color map_get($vars, $transition-time), + transition: background-color linear map_get($vars, $transition-time), color map_get($vars, $transition-time); hyphens: auto; - - touch-action: manipulation; } img, @@ -66,7 +57,6 @@ .figure-container { width: 100%; height: auto; - object-fit: contain; } .figure-container { @@ -97,35 +87,34 @@ 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; } } - max-height: 100%; // to take mobile nav-bar into account - overflow-x: hidden; - overflow-y: scroll; + div#main { + height: 100%; + overflow-x: hidden; + overflow-y: scroll; - //will-change: transform; - - perspective: 5px; - perspective-origin: center center; - - noscript { - @include square(100%); - @include center-children(); - } - - @include on-large-screen { - &::-webkit-scrollbar-track, - &::-webkit-scrollbar { - background-color: transparent; - width: 12px; + noscript { + @include square(100%); + @include center-children(); } - &::-webkit-scrollbar-thumb { - background-color: map_get($vars, $accent-color); - border-radius: map_get($vars, $border-radius); + + @include on-large-screen { + &::-webkit-scrollbar-track, + &::-webkit-scrollbar { + background-color: transparent; + width: 12px; + } + &::-webkit-scrollbar-thumb { + background-color: map_get($vars, $accent-color); + border-radius: map_get($vars, $border-radius); + } } } }