diff --git a/src/app/app.component.html b/src/app/app.component.html index 9095485..5ddda08 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -19,54 +19,56 @@
+ -
- +
+
+

+

+
-
-
-
-

-

-
-
+
+ +
+ + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + +
+ +
- -
- -
+ -
+
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 4424760..75dfcc2 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,4 +1,3 @@ -@use 'style/language-selector'; @use 'style/lenis'; #background-img { @@ -60,6 +59,19 @@ flex-direction: column; text-align: center; align-items: center; + + // for rotation + position: relative; + transform-style: preserve-3d; + transform-origin: left; + transition: all .6s ease-in-out; + &.rotated { + transition: all .4s ease-in-out; + //transform: rotateY(calc(1 * var(--rotate-angle))); + //transform: rotateX(70deg); + //transform: translateY(-5000px); + //transform: scale(0.6); + } } :host ::ng-deep { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5f62c0e..cf1a109 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -11,6 +11,8 @@ import 'js-circle-progress'; import {TimelineCardComponent} from "./components/timeline-card/timeline-card.component"; import {HeadingCardComponent} from "./components/heading-card/heading-card.component"; import {SkillsCardComponent} from "./components/skills-card/skills-card.component"; +import {NavbarDotComponent} from "./components/navbar-dot/navbar-dot.component"; +import {SidebarComponent} from "./sections/sidebar/sidebar.component"; gsap.registerPlugin(ScrollTrigger); @@ -29,7 +31,10 @@ import "./js/lenis.js"; // Custom TimelineCardComponent, HeadingCardComponent, - SkillsCardComponent], + SkillsCardComponent, + NavbarDotComponent, + SidebarComponent + ], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) @@ -38,8 +43,9 @@ export class AppComponent { @ViewChild("backgroundImg", {read: ElementRef}) backgroundImage: ElementRef; @ViewChild("outest_container", {read: ElementRef}) outestContainer: ElementRef; @ViewChild("progress_bar", {read: ElementRef}) progressBar: ElementRef; + // @ViewChild("content_container", {read: ElementRef}) contentContainer: ElementRef; @ViewChild("education_card", {read: ElementRef}) educationCard: ElementRef; - @ViewChild("lang_selector", {read: ElementRef}) langSelector: ElementRef; + @ViewChild("sidebar", {read: ElementRef}) sidebar: ElementRef; dataService: DataService; @@ -51,7 +57,7 @@ export class AppComponent { } ngAfterViewInit(): void { - this.addLangButtonAnimation(); + this.addSidebarAnimation(); this.addProgressBarAnimation(); // @ts-ignore for (let el of document.getElementsByClassName("alarm-clock-animated")) { @@ -86,7 +92,7 @@ export class AppComponent { }) } - addLangButtonAnimation() { + addSidebarAnimation() { const tl: gsap.core.Timeline = gsap.timeline({ scrollTrigger: { start: "top 90%", @@ -99,12 +105,12 @@ export class AppComponent { .to(this.backgroundImage.nativeElement, { filter: "blur(2px)" }) - .fromTo(this.langSelector.nativeElement, { + .fromTo(this.sidebar.nativeElement, { opacity: 0, ease: "power1.in", display: "none" }, { - display: "var(--display-side-elements)", + display: "var(--display-side-elements-flex)", opacity: 1, }, "<"); } @@ -115,7 +121,7 @@ export class AppComponent { start: "top bottom", trigger: el, end: "center center", - // markers: true, // TODO remove + // markers: true, scrub: true, }, defaults: { @@ -127,7 +133,7 @@ export class AppComponent { start: "center center", trigger: el, end: "bottom top", - // markers: true, // TODO remove + // markers: true, scrub: true, }, defaults: { diff --git a/src/app/components/navbar-dot/navbar-dot.component.html b/src/app/components/navbar-dot/navbar-dot.component.html new file mode 100644 index 0000000..b557c35 --- /dev/null +++ b/src/app/components/navbar-dot/navbar-dot.component.html @@ -0,0 +1 @@ + diff --git a/src/app/components/navbar-dot/navbar-dot.component.scss b/src/app/components/navbar-dot/navbar-dot.component.scss new file mode 100644 index 0000000..8607dfa --- /dev/null +++ b/src/app/components/navbar-dot/navbar-dot.component.scss @@ -0,0 +1,74 @@ +:host { + height: 1.2rem; + width: 1.2rem; + border-radius: 50%; + border: 3px solid rgb(255, 255, 255); + transition: all .6s ease-in-out; + user-select: none; + + display: flex; + align-items: center; + justify-content: center; + font-size: 0; + + &:not(.extended):hover { + transition: all .1s ease-in-out; + height: 1.5rem; + width: 1.5rem; + border: 5px solid rgb(255, 255, 255); + background: rgba(0, 0, 0, 0.3); + cursor: pointer; + } + + &.active:not(.extended) { + background: #e134e8 !important; + } + &.extended { + position: relative; + transition: all .4s ease-in-out; + width: 20rem; + height: 5rem; + margin: 0 3rem; + border-radius: 20px; + //transform: translateX(-4rem); + font-size: 1.3rem; + &.active { + border: 0; + &::before { + // code from: https://dev.to/afif/border-with-gradient-and-radius-387f + content: ""; + position: absolute; + inset: 0; + border-radius: 20px; + padding: 5px; + margin: -5px; + background: linear-gradient(to right, #a445b2, #fa4299); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + } + } + &:hover { + width: 21rem; + margin: 0 1rem; + height: 5.3rem; + font-size: 1.4rem; + background: rgba(0, 0, 0, 0.3); + cursor: pointer; + } + span { + display: block; + } + } +} + +span { + display: none; + transition: all .4s ease-in-out; + color: white; + text-align: center; + margin-top: auto; + margin-bottom: auto; +} diff --git a/src/app/components/navbar-dot/navbar-dot.component.ts b/src/app/components/navbar-dot/navbar-dot.component.ts new file mode 100644 index 0000000..5316df8 --- /dev/null +++ b/src/app/components/navbar-dot/navbar-dot.component.ts @@ -0,0 +1,56 @@ +import {Component, EventEmitter, HostBinding, HostListener, Input, Output} from '@angular/core'; +import {Section} from "../../data/model"; +import gsap from "gsap"; +import {ScrollToPlugin} from "gsap/ScrollToPlugin"; + +gsap.registerPlugin(ScrollToPlugin) + +@Component({ + selector: 'navbar-dot', + standalone: true, + imports: [], + templateUrl: './navbar-dot.component.html', + styleUrl: './navbar-dot.component.scss' +}) +export class NavbarDotComponent { + + @Input({required: true}) section: Section; + + // @HostBinding('class.extended') + // @Input() + // extended = false; + + @HostBinding('class.active') + @Input() + active = false; + + // @Output() hover: EventEmitter = new EventEmitter(); + + delayTimeout: any; + + // @HostListener("mouseenter") + // onMouseover() { + // this.delayTimeout = setTimeout(() => this.hover.emit(), 300); + // } + + /** + * In case the hovering stops before delay timeout is over, the hovering is not forwarded. + */ + // @HostListener("mouseleave") + // onMouseleave() { + // clearTimeout(this.delayTimeout); + // } + + @HostListener("click") + onClick() { + gsap.to(window, { + duration: 2, + scrollTo: { + y: `#${this.section.id}`, + offsetY: 20, + }, + ease: "power1.inOut" + }); + } + +} diff --git a/src/app/data/data.service.ts b/src/app/data/data.service.ts index b3c8887..d30aa0c 100644 --- a/src/app/data/data.service.ts +++ b/src/app/data/data.service.ts @@ -33,7 +33,7 @@ export class DataService { // @ts-ignore this.education = educationJson[this.lang]; // @ts-ignore - this.languagePack = generalJson[this.lang]; + this.languagePack = new LanguagePack(generalJson[this.lang]); // @ts-ignore this.experience = experienceJson[this.lang]; // @ts-ignore diff --git a/src/app/data/model.ts b/src/app/data/model.ts index 32f98be..b3fe537 100644 --- a/src/app/data/model.ts +++ b/src/app/data/model.ts @@ -33,7 +33,7 @@ export interface SkillCategory { skills: Skill[]; } -export interface LanguagePack { +export interface ILanguagePack { /** * This is the identifier which is used in the languages array and for definition of the object. */ @@ -46,7 +46,51 @@ export interface LanguagePack { isoAlpha2: string; heading: string; subheading: string; + home: string; education: string; experience: string; skills: string; + about: string; +} + +export class LanguagePack implements ILanguagePack { + id: string; + name: string; + isoAlpha2: string; + heading: string; + subheading: string; + home: string; + education: string; + experience: string; + skills: string; + about: string; + + sections: Section[]; + + constructor(langPack: ILanguagePack) { + this.id = langPack.id; + this.name = langPack.name; + this.isoAlpha2 = langPack.isoAlpha2; + this.heading = langPack.heading; + this.subheading = langPack.subheading; + this.home = langPack.home; + this.education = langPack.education; + this.experience = langPack.experience; + this.skills = langPack.skills; + this.about = langPack.about; + + this.sections = [ + {name: this.home, id: "home-start", position: 0}, + {name: this.education, id: "education-start", position: 1}, + {name: this.experience, id: "experience-start", position: 2}, + {name: this.skills, id: "skills-start", position: 3}, + {name: this.about, id: "about-start", position: 4}, + ]; + } +} + +export interface Section { + name: string; + id: string; + position: number; } diff --git a/src/app/sections/sidebar/sidebar.component.html b/src/app/sections/sidebar/sidebar.component.html new file mode 100644 index 0000000..4f4fca8 --- /dev/null +++ b/src/app/sections/sidebar/sidebar.component.html @@ -0,0 +1,15 @@ + + + diff --git a/src/app/style/language-selector.scss b/src/app/sections/sidebar/sidebar.component.scss similarity index 56% rename from src/app/style/language-selector.scss rename to src/app/sections/sidebar/sidebar.component.scss index 0e6a27d..c2a349b 100644 --- a/src/app/style/language-selector.scss +++ b/src/app/sections/sidebar/sidebar.component.scss @@ -1,3 +1,41 @@ +:host { + position: absolute; + z-index: 1000; +} + +.sidebar-container { + position: fixed; + width: auto; + min-width: 5vw; + max-width: 30vw; + top: 0; + right: 0; + + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; +} + +#lang-selector { + position: absolute; + top: 2vh; + transform: translateX(-32px); + //align-self: flex-end; +} + +#navbar { + //position: absolute; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + //flex-grow: 1; +} + +// Lang Button :host ::ng-deep { .flag-icon { width: 100%; @@ -31,11 +69,3 @@ } } } - -#lang-selector { - position: fixed; - right: calc(2vh + 64px); - top: 2vh; - z-index: 1000; - display: var(--display-side-elements); -} diff --git a/src/app/sections/sidebar/sidebar.component.ts b/src/app/sections/sidebar/sidebar.component.ts new file mode 100644 index 0000000..6bcfcf7 --- /dev/null +++ b/src/app/sections/sidebar/sidebar.component.ts @@ -0,0 +1,75 @@ +import { + ChangeDetectorRef, + Component, + HostListener, + Input, + Output, + QueryList, + ViewChildren +} from '@angular/core'; +import {NavbarDotComponent} from "../../components/navbar-dot/navbar-dot.component"; +import {NgForOf} from "@angular/common"; +import {SpeedDialModule} from "primeng/speeddial"; +import {Section} from "../../data/model"; +import {MenuItem} from "primeng/api"; +import {ScrollTrigger} from "gsap/ScrollTrigger"; + +@Component({ + selector: 'sidebar', + standalone: true, + imports: [ + NavbarDotComponent, + NgForOf, + SpeedDialModule + ], + templateUrl: './sidebar.component.html', + styleUrl: './sidebar.component.scss' +}) +export class SidebarComponent { + + @Input({required: true}) sections: Section[]; + @Input({required: true}) languagesMenuItems: MenuItem[]; + + // @Output() hovered: boolean = false; + + @ViewChildren("navbar_dot", {read: NavbarDotComponent}) dots: QueryList; + + sectionActive = 0; + + changeDetectorRef: ChangeDetectorRef; + + constructor(changeDetectorRef: ChangeDetectorRef) { + this.changeDetectorRef = changeDetectorRef; + } + + ngAfterViewInit(): void { + this.addSelectedAnimation(); + } + + // @HostListener("mouseleave") + // onMouseleave() { + // this.hovered = false; + // } + + addSelectedAnimation() { + for (let i = 0; i < this.sections.length; i++) { + const section = this.sections[i]; + ScrollTrigger.create({ + start: "top 50%", + trigger: `#${section.id}`, + end: "+=1", + // markers: true, + onEnter: self => { + console.log("Active section is ", i, "onEnter"); + this.sectionActive = i; + this.changeDetectorRef.detectChanges(); + }, + onEnterBack: self => { + console.log("Active section is ", i-1, "onEnterBack"); + this.sectionActive = i-1; + this.changeDetectorRef.detectChanges(); + } + }); + } + } +} diff --git a/src/data/general.json b/src/data/general.json index 00e2efa..fdd23b0 100644 --- a/src/data/general.json +++ b/src/data/general.json @@ -7,9 +7,11 @@ "isoAlpha2": "de", "heading": "Axel Herrmann", "subheading": "Software Engineer", + "home": "Home", "education": "Bildungsweg", "experience": "Erfahrung", - "skills": "Kenntnisse" + "skills": "Kenntnisse", + "about": "Über" }, "en": { "id": "en", @@ -17,8 +19,10 @@ "isoAlpha2": "us", "heading": "Axel Herrmann", "subheading": "Software Engineer", + "home": "Home", "education": "Education", "experience": "Experience", - "skills": "Skills" + "skills": "Skills", + "about": "About" } } diff --git a/src/styles.scss b/src/styles.scss index e8a9b82..4afaf50 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -45,10 +45,37 @@ html { @media (max-width: 1000px) { --main-content-width: 100vw; --display-side-elements: none; + --display-side-elements-flex: none; } @media (min-width: 1000px) { --main-content-width: max(60vw, 1000px); --display-side-elements: block; + --display-side-elements-flex: flex; } --main-content-side-margin: calc((100vw - var(--main-content-width)) / 2); + + /******************************************************* + * Rotation calculations * + *******************************************************/ + /* Input */ + --side-content-width-visible-percent: calc(4/5); + + /* Calculations */ + --rotated-depth-percent: calc(sqrt(1 - pow(var(--side-content-width-visible-percent),2))); + /* alpha = acos(ancathete / hypotenuse) */ + --rotate-angle: calc(acos(var(--side-content-width-visible-percent))); + + /* + (180 - alpha) / 2 + => result has to be subtracted from 180 again because of css rotation rules + */ + --side-content-rotation: calc(180deg - (180deg - var(--rotate-angle)) / 2); + /* + a² = b² + c² - 2bc * cos(alpha) + a = √(b² + c² - 2bc * cos(alpha)) + a = √(1² + 1² - 2 * cos(alpha)) + a = √(2 - 2cos(alpha)) + */ + /*--side-content-length-percent: calc(2 - 2 * sqrt(cos(var(--rotate-angle))));*/ + --side-content-length-percent: sqrt(2 - 2 * cos(var(--rotate-angle))); }