From 5f143d5fac66dd4bd5dedf977216819b5309727d Mon Sep 17 00:00:00 2001 From: bryancito1090 Date: Thu, 3 Sep 2026 12:51:09 -0500 Subject: [PATCH] feat: Refactor technical profile component to enhance user experience and add digital rain effect - Replaced static HTML content with dynamic rendering using Angular's template syntax. - Introduced a new section for "About Me" with ASCII art and descriptive paragraphs. - Implemented a digital rain effect on a canvas element, enhancing visual appeal. - Added responsive design considerations and improved accessibility features. - Updated TypeScript component to manage new functionalities and lifecycle hooks. - Included a new text asset for ASCII art representation. - Created a new skill review guideline for code quality assessment. --- .agents/skills/ponytail-review/SKILL.md | 30 ++ .../app/application/i18n/translations/en.ts | 6 +- .../app/application/i18n/translations/es.ts | 4 + .../src/app/domain/models/i18n.model.ts | 4 + .../features/landing/landing.component.css | 17 ++ .../features/landing/landing.component.html | 6 +- .../technical-profile.component.css | 259 ++++++++++++------ .../technical-profile.component.html | 94 ++----- .../technical-profile.component.ts | 259 +++++++++++++++++- src/frontend/src/assets/portfolio.txt | 93 +++++++ src/frontend/tsconfig.app.json | 1 + 11 files changed, 617 insertions(+), 156 deletions(-) create mode 100644 .agents/skills/ponytail-review/SKILL.md create mode 100644 src/frontend/src/assets/portfolio.txt diff --git a/.agents/skills/ponytail-review/SKILL.md b/.agents/skills/ponytail-review/SKILL.md new file mode 100644 index 0000000..96a22b7 --- /dev/null +++ b/.agents/skills/ponytail-review/SKILL.md @@ -0,0 +1,30 @@ +--- +name: ponytail-review +description: > + Code review focused exclusively on over-engineering. Finds what to delete: + reinvented standard library, unneeded dependencies, speculative abstractions, + and dead flexibility. +--- + +# Ponytail Review + +Review the current diff for unnecessary complexity only. Do not review +correctness, security, performance, or style. + +Report one line per finding using: + +`:L: . .` + +Allowed tags: + +- `delete`: dead code or speculative feature; replacement is nothing. +- `stdlib`: hand-rolled functionality provided by the standard library. +- `native`: code or dependency replaceable by a platform feature. +- `yagni`: abstraction with one implementation or one caller. +- `shrink`: same logic expressed with fewer lines. + +End with `net: - lines possible.` If there is nothing to cut, report +`Lean already. Ship.` + +The review does not apply fixes. A smoke test is the minimum acceptable +validation and must not be flagged for deletion. diff --git a/src/frontend/src/app/application/i18n/translations/en.ts b/src/frontend/src/app/application/i18n/translations/en.ts index 1d57850..bc89b8f 100644 --- a/src/frontend/src/app/application/i18n/translations/en.ts +++ b/src/frontend/src/app/application/i18n/translations/en.ts @@ -50,7 +50,11 @@ export const EN_TRANSLATIONS: TranslationDictionary = { p3RightBadge2: 'Live .NET API', // Technical Profile - tpEyebrow: '01 // COMPUTER ENGINEERING & EXPERTISE', + tpAboutGreeting: 'Hello, I am', + tpAboutName: 'Bryan Baño', + tpAboutDescription: 'I am a software engineer focused on building thoughtful, accessible, and technically solid digital experiences. I combine clean architecture, modern frontend development, and scalable backend solutions. I work with .NET, Angular, Signals, REST APIs, and relational databases.\n\nMy approach combines engineering and design. First I understand the problem, then simplify the experience, and finally build a maintainable solution. I prioritize performance, accessibility, separation of concerns, and code quality.\n\nI also explore interfaces inspired by Unix terminals, retro systems, Cyber-Zen aesthetics, and new ways to present technical information without sacrificing readability. Every project must be functional, expressive, and easy to maintain.\n\nI currently develop solutions that connect frontend, backend, visual design, and digital interaction. I am interested in creating products that not only work reliably, but also communicate a clear and memorable identity.', + tpAboutAria: 'About Bryan Baño', + tpEyebrow: 'INGENIERÍA EN COMPUTACIÓN & EXPERTISE', tpTitle: 'Technical Skills & Areas of Interest', tpLeadStudent: '9th semester Computer Engineering student at', tpLeadSpecialization: 'Specialized in clean architectures with', diff --git a/src/frontend/src/app/application/i18n/translations/es.ts b/src/frontend/src/app/application/i18n/translations/es.ts index 8f975fa..efc16a8 100644 --- a/src/frontend/src/app/application/i18n/translations/es.ts +++ b/src/frontend/src/app/application/i18n/translations/es.ts @@ -50,6 +50,10 @@ export const ES_TRANSLATIONS: TranslationDictionary = { p3RightBadge2: 'Live .NET API', // Technical Profile + tpAboutGreeting: 'Hola, soy', + tpAboutName: 'Bryan Baño', + tpAboutDescription: 'Soy ingeniero de software enfocado en construir experiencias digitales claras, accesibles y técnicamente sólidas. Me interesa combinar arquitectura limpia, desarrollo frontend moderno y soluciones backend escalables. Trabajo con .NET, Angular, Signals, APIs REST y bases de datos relacionales.\n\nMi proceso combina ingeniería y diseño. Primero entiendo el problema, después simplifico la experiencia y finalmente construyo una solución mantenible. Priorizo el rendimiento, la accesibilidad, la separación de responsabilidades y la calidad del código.\n\nTambién exploro interfaces inspiradas en terminales Unix, sistemas retro, estética Cyber-Zen y nuevas formas de presentar información técnica sin sacrificar legibilidad. Cada proyecto debe ser funcional, expresivo y fácil de mantener.\n\nActualmente desarrollo soluciones que conectan frontend, backend, diseño visual e interacción digital. Me interesa crear productos que no solo funcionen correctamente, sino que también comuniquen una identidad clara y memorable.', + tpAboutAria: 'Sobre Bryan Baño', tpEyebrow: 'INGENIERÍA EN COMPUTACIÓN & EXPERTISE', tpTitle: 'Habilidades Técnicas & Áreas de Interés', tpLeadStudent: 'Estudiante de 9no semestre de Ingeniería en Computación en la', diff --git a/src/frontend/src/app/domain/models/i18n.model.ts b/src/frontend/src/app/domain/models/i18n.model.ts index de99a3b..4a0becf 100644 --- a/src/frontend/src/app/domain/models/i18n.model.ts +++ b/src/frontend/src/app/domain/models/i18n.model.ts @@ -50,6 +50,10 @@ export interface TranslationDictionary { readonly p3RightBadge2: string; // Technical Profile + readonly tpAboutGreeting: string; + readonly tpAboutName: string; + readonly tpAboutDescription: string; + readonly tpAboutAria: string; readonly tpEyebrow: string; readonly tpTitle: string; readonly tpLeadStudent: string; diff --git a/src/frontend/src/app/presentation/features/landing/landing.component.css b/src/frontend/src/app/presentation/features/landing/landing.component.css index a89c573..b0b6575 100644 --- a/src/frontend/src/app/presentation/features/landing/landing.component.css +++ b/src/frontend/src/app/presentation/features/landing/landing.component.css @@ -140,6 +140,23 @@ flex-direction: column; } +.profile-module-card app-technical-profile { + display: block; + height: 100%; +} + +.matrix-stage { + width: 100%; + height: clamp(64px, 10vh, 104px); + background: #121517; +} + +.matrix-stage app-technical-profile { + display: block; + width: 100%; + height: 100%; +} + /* ============================================================================== SECCIÓN DE PROYECTOS DESTACADOS (CASOS REALES A ANCHO COMPLETO) ============================================================================== */ diff --git a/src/frontend/src/app/presentation/features/landing/landing.component.html b/src/frontend/src/app/presentation/features/landing/landing.component.html index fc5ca1d..c5a6d92 100644 --- a/src/frontend/src/app/presentation/features/landing/landing.component.html +++ b/src/frontend/src/app/presentation/features/landing/landing.component.html @@ -28,7 +28,7 @@
- +
@@ -38,6 +38,10 @@
+
+ +
+
diff --git a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.css b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.css index 2f9337c..d83d132 100644 --- a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.css +++ b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.css @@ -7,11 +7,12 @@ .profile-editorial-content { width: 100%; height: 100%; + min-height: 0; display: flex; flex-direction: column; justify-content: center; - gap: var(--space-4); - padding: var(--space-2) 0; + gap: 0; + padding: 0; background: transparent; border: none; box-shadow: none; @@ -19,118 +20,208 @@ box-sizing: border-box; } +.about-me { + display: flow-root; + width: 100%; + min-height: 0; + padding: 0; + container-type: inline-size; +} + +.about-copy { + display: block; + min-width: 0; + padding: 0; + line-height: 1.65; +} + /* Eyebrow */ .editorial-eyebrow { + display: block; + margin: 0 0 var(--space-1) 0; font-family: var(--font-sans); - font-size: 0.75rem; + font-size: clamp(0.85rem, 2cqw, 1.1rem); font-weight: 700; - letter-spacing: 0.12em; + letter-spacing: 0.16em; color: var(--zen-accent-kintsugi); text-transform: uppercase; - display: inline-block; +} + +.about-greeting { + margin: 0; + color: var(--zen-text-secondary); + font-size: clamp(0.95rem, 1.8cqw, 1.125rem); + font-weight: 500; + line-height: 1.35; } /* Título Principal H2 */ .editorial-title { + margin: 0 0 var(--space-4) 0; font-family: var(--font-sans); - font-size: clamp(1.85rem, 3vw, 2.35rem); - font-weight: 800; + font-size: clamp(2rem, 5cqw, 3rem); + font-weight: 900; color: #ffffff; - line-height: 1.15; - letter-spacing: -0.02em; - margin: 0; + line-height: 1.1; + letter-spacing: -0.03em; } -/* Párrafo Lead */ -.editorial-lead { - font-family: var(--font-sans); - font-size: 0.9375rem; +.about-description { + margin: 0 0 var(--space-4) 0; color: var(--zen-text-secondary); - line-height: 1.6; + font-size: clamp(0.875rem, 1.55cqw, 1rem); + line-height: 1.7; +} + +.about-link { + display: inline-block; + margin-top: var(--space-2); + color: var(--zen-text-gold); + font-family: var(--font-sans); + font-size: clamp(1rem, 2cqw, 1.25rem); + font-weight: 700; +} + +.ascii-portrait { + float: left; + width: 58%; + min-width: 0; + margin: var(--space-2) 0 0 0; + padding: 0; + overflow: visible; + color: var(--zen-text-gold); + font-family: var(--font-mono); + font-size: clamp(1.8px, calc(58cqw / 108), 4.2px); + line-height: 0.84; + text-align: left; + shape-outside: polygon( + 0% 0%, + 49.4% 0%, + 52.2% 4.3%, + 59.4% 8.6%, + 62.8% 16.1%, + 65.0% 32.3%, + 63.3% 36.6%, + 58.9% 39.8%, + 57.2% 44.1%, + 58.3% 49.5%, + 63.3% 53.8%, + 70.6% 57.0%, + 80.6% 60.2%, + 90.0% 63.4%, + 91.7% 66.7%, + 93.3% 73.1%, + 95.0% 82.8%, + 97.8% 92.5%, + 100% 100%, + 0% 100% + ); + shape-margin: 20px; + user-select: none; +} + +.ascii-portrait pre { margin: 0; + padding: 0; + width: auto; + max-width: 100%; + white-space: pre; + font-family: inherit; + font-size: inherit; + line-height: inherit; + text-align: left; } -.editorial-lead strong { - color: #ffffff; - font-weight: 600; +.ascii-status { + display: block; + max-width: 120px; + font-family: var(--font-mono); + font-size: 0.65rem; + line-height: 1.4; + color: var(--zen-text-muted); } -/* Secciones de Contenido */ -.editorial-section { - display: flex; - flex-direction: column; - gap: var(--space-2); +.hacker-screen { + flex: 0 0 clamp(64px, 10vh, 104px); + width: 100%; + overflow: hidden; + position: relative; + background: none; + background-color: transparent; + font-family: var(--font-mono); + isolation: isolate; + border: 0; + box-shadow: none; } -.section-title-row { - display: flex; - align-items: center; - gap: 6px; +.hacker-rain { + position: absolute; + inset: 0; + z-index: 0; + display: block; + background: transparent; + height: 100%; + width: 100%; + overflow: hidden; + pointer-events: none; + font-family: var(--font-mono); } -.section-bullet { - color: var(--zen-accent-kintsugi); - font-size: 0.875rem; - font-weight: 700; +@media (max-width: 960px) { + .ascii-portrait { + width: 56%; + shape-margin: 16px; + font-size: clamp(1.8px, calc(56cqw / 108), 3.6px); + } + + .editorial-eyebrow { + font-size: clamp(0.8rem, 1.8cqw, 1rem); + } + + .editorial-title { + font-size: clamp(1.8rem, 4.5cqw, 2.6rem); + } } -.editorial-subtitle { - font-family: var(--font-sans); - font-size: 1.15rem; - font-weight: 700; - color: #ffffff; - margin: 0; - letter-spacing: -0.01em; -} +@media (max-width: 640px) { + .profile-editorial-content { + height: auto; + } -/* Grid de Categorías Técnicas */ -.skills-grid { - display: flex; - flex-direction: column; - gap: 6px; - padding-left: 14px; -} + .about-me { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--space-4) 0; + min-height: auto; + } -.skill-category { - font-family: var(--font-sans); - font-size: 0.8125rem; - line-height: 1.5; - color: var(--zen-text-secondary); -} + .about-copy { + width: 100%; + padding: 0; + } -.cat-label { - font-family: var(--font-sans); - font-weight: 700; - color: #7dcfff; - font-size: 0.8125rem; - margin-right: 6px; -} + .editorial-eyebrow { + font-size: clamp(0.75rem, 3.5vw, 0.95rem); + } -.cat-techs { - color: #c0caf5; -} + .editorial-title { + font-size: clamp(1.6rem, 7vw, 2.2rem); + } -/* Lista de Áreas de Interés */ -.interests-list { - list-style: none; - padding: 0 0 0 14px; - margin: 0; - display: flex; - flex-direction: column; - gap: 6px; -} + .ascii-portrait { + float: none; + width: 100%; + max-width: 380px; + margin: var(--space-6) auto 0 auto; + shape-outside: none; + font-size: clamp(1.6px, calc(95cqw / 108), 3.2px); + text-align: center; + } -.interest-item { - font-family: var(--font-sans); - font-size: 0.8125rem; - line-height: 1.5; - color: var(--zen-text-secondary); -} - -.interest-key { - font-family: var(--font-sans); - font-weight: 700; - color: #bb9af7; - font-size: 0.8125rem; - margin-right: 6px; + .ascii-portrait pre { + margin: 0 auto; + text-align: left; + display: inline-block; + } } diff --git a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html index d1c15b1..c8667db 100644 --- a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html +++ b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html @@ -1,74 +1,32 @@
- - - {{ i18n.t().tpEyebrow }} - + @if (showProfile()) { +
+
+ {{ i18n.t().tpEyebrow }} +

{{ i18n.t().tpAboutGreeting }}

- -

- {{ i18n.t().tpTitle }} -

+
+ @if (asciiArt()) { +
{{ asciiArt() }}
+ } @else if (asciiLoadError()) { +
No se pudo cargar el retrato.
+ } @else { +
Cargando retrato...
+ } +
- -

- {{ i18n.t().tpLeadStudent }} Escuela Politécnica Nacional (EPN). - {{ i18n.t().tpLeadSpecialization }} {{ i18n.t().tpLeadStack }}{{ i18n.t().tpLeadEnd }} -

- - -
-
- ▹ -

{{ i18n.t().tpSkillsSubtitle }}

-
- -
- -
- {{ i18n.t().tpCatBackend }} - .NET 8/9, C#, Clean Architecture, CQRS (MediatR), FluentValidation, REST APIs. +

{{ i18n.t().tpAboutName }}

+ @for (paragraph of aboutParagraphs(); track $index) { +

{{ paragraph }}

+ } + About me
+
+ } - -
- {{ i18n.t().tpCatFrontend }} - Angular 16+, Signals reactivos, ChangeDetection OnPush, TypeScript, Canvas 2D. -
- - -
- {{ i18n.t().tpCatData }} - MySQL, PostgreSQL, SQLite, Docker, AWS (Academy Graduate), Git/GitHub CI/CD. -
- - -
- {{ i18n.t().tpCatSecurity }} - {{ i18n.t().tpCatSecurityDesc }} -
-
-
- - -
-
- ▹ -

{{ i18n.t().tpInterestsSubtitle }}

-
- -
    -
  • - {{ i18n.t().tpInterest1Key }} - {{ i18n.t().tpInterest1Desc }} -
  • -
  • - {{ i18n.t().tpInterest2Key }} - {{ i18n.t().tpInterest2Desc }} -
  • -
  • - {{ i18n.t().tpInterest3Key }} - {{ i18n.t().tpInterest3Desc }} -
  • -
-
+ @if (showMatrix()) { +
+ +
+ } diff --git a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.ts b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.ts index a1e86e3..cdd41f5 100644 --- a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.ts +++ b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.ts @@ -1,6 +1,32 @@ -import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + computed, + ElementRef, + inject, + input, + OnDestroy, + OnInit, + signal, + ViewChild +} from '@angular/core'; import { I18nService } from '../../../application/services/i18n.service'; +interface DigitalRainColumn { + x: number; + y: number; + speed: number; + length: number; + opacity: number; + characters: string[]; +} + +const BINARY_CHARACTERS = '01'; +const HEX_CHARACTERS = '0123456789ABCDEF'; +const CODE_CHARACTERS = '{}[]()<>&|/\\:;=+-_*#$%@?!~^'; +const TERMINAL_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + @Component({ selector: 'app-technical-profile', standalone: true, @@ -8,6 +34,235 @@ import { I18nService } from '../../../application/services/i18n.service'; styleUrl: './technical-profile.component.css', changeDetection: ChangeDetectionStrategy.OnPush }) -export class TechnicalProfileComponent { +export class TechnicalProfileComponent implements AfterViewInit, OnDestroy, OnInit { + @ViewChild('rainCanvas') private readonly rainCanvas?: ElementRef; + protected readonly i18n = inject(I18nService); + readonly showProfile = input(true); + readonly showMatrix = input(true); + protected readonly asciiArt = signal(''); + protected readonly asciiLoadError = signal(false); + protected readonly aboutParagraphs = computed(() => + this.i18n.t().tpAboutDescription.split('\n\n').filter((p) => p.trim().length > 0) + ); + private readonly columns: DigitalRainColumn[] = []; + private context?: CanvasRenderingContext2D; + private animationFrameId?: number; + private intersectionObserver?: IntersectionObserver; + private resizeObserver?: ResizeObserver; + private motionQuery?: MediaQueryList; + private isVisible = true; + private isReducedMotion = false; + private lastFrameTime = 0; + + ngOnInit(): void { + if (!this.showProfile()) { + return; + } + + this.loadTextAsset('assets/portfolio.txt') + .then((portrait) => this.asciiArt.set(portrait)) + .catch(() => this.asciiLoadError.set(true)); + } + + private loadTextAsset(path: string): Promise { + return fetch(path).then((response) => { + if (!response.ok) { + throw new Error(`ASCII asset request failed: ${response.status}`); + } + + return response.text(); + }); + } + + ngAfterViewInit(): void { + if (!this.showMatrix() || !this.rainCanvas) { + return; + } + + const canvas = this.rainCanvas.nativeElement; + this.context = canvas.getContext('2d') ?? undefined; + if (!this.context) { + return; + } + + this.motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + this.isReducedMotion = this.motionQuery.matches; + this.motionQuery.addEventListener('change', this.onMotionPreferenceChange); + + this.resizeObserver = new ResizeObserver(() => this.resizeCanvas()); + this.resizeObserver.observe(canvas.parentElement ?? canvas); + + this.intersectionObserver = new IntersectionObserver(([entry]) => { + this.isVisible = entry.isIntersecting; + if (this.isVisible && !this.isReducedMotion && this.animationFrameId === undefined) { + this.lastFrameTime = 0; + this.animationFrameId = requestAnimationFrame(this.animate); + } + }, { threshold: 0.05 }); + this.intersectionObserver.observe(canvas); + + this.resizeCanvas(); + if (!this.isReducedMotion) { + this.animationFrameId = requestAnimationFrame(this.animate); + } + } + + ngOnDestroy(): void { + if (this.animationFrameId !== undefined) { + cancelAnimationFrame(this.animationFrameId); + } + this.motionQuery?.removeEventListener('change', this.onMotionPreferenceChange); + this.intersectionObserver?.disconnect(); + this.resizeObserver?.disconnect(); + } + + private readonly onMotionPreferenceChange = (event: MediaQueryListEvent): void => { + this.isReducedMotion = event.matches; + if (this.isReducedMotion) { + if (this.animationFrameId !== undefined) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = undefined; + } + this.draw(); + return; + } + + if (this.isVisible && this.animationFrameId === undefined) { + this.lastFrameTime = 0; + this.animationFrameId = requestAnimationFrame(this.animate); + } + }; + + private readonly animate = (timestamp: number): void => { + this.animationFrameId = undefined; + if (!this.isVisible || this.isReducedMotion) { + return; + } + + const elapsed = this.lastFrameTime === 0 ? 0 : Math.min(timestamp - this.lastFrameTime, 50); + this.lastFrameTime = timestamp; + this.update(elapsed / 1000); + this.draw(); + this.animationFrameId = requestAnimationFrame(this.animate); + }; + + private resizeCanvas(): void { + if (!this.rainCanvas) { + return; + } + + const canvas = this.rainCanvas.nativeElement; + const bounds = canvas.getBoundingClientRect(); + const pixelRatio = Math.min(window.devicePixelRatio || 1, 2); + canvas.width = Math.max(1, Math.floor(bounds.width * pixelRatio)); + canvas.height = Math.max(1, Math.floor(bounds.height * pixelRatio)); + this.context?.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + this.createColumns(bounds.width, bounds.height); + this.draw(); + } + + private createColumns(width: number, height: number): void { + const fontSize = Math.max(14, Math.min(20, width / 58)); + const columnCount = Math.max(24, Math.floor(width / (fontSize * 0.72))); + this.columns.length = 0; + + for (let index = 0; index < columnCount; index += 1) { + const length = this.randomInteger(14, 38); + this.columns.push({ + x: (index / columnCount) * width + Math.random() * fontSize * 0.6, + y: Math.random() * height * 1.4 - height, + speed: this.randomInteger(150, 280), + length, + opacity: 0.16 + Math.random() * 0.68, + characters: Array.from({ length }, () => this.randomCharacter()) + }); + } + } + + private update(deltaSeconds: number): void { + if (!this.rainCanvas) { + return; + } + + const height = this.rainCanvas.nativeElement.clientHeight; + const fontSize = Math.max(14, Math.min(20, this.rainCanvas.nativeElement.clientWidth / 58)); + + for (const column of this.columns) { + column.y += column.speed * deltaSeconds; + if (column.y - column.length * fontSize > height) { + column.y = -Math.random() * height * 0.75 - column.length * fontSize; + column.speed = this.randomInteger(150, 280); + column.length = this.randomInteger(14, 38); + column.opacity = 0.16 + Math.random() * 0.68; + column.characters = Array.from({ length: column.length }, () => this.randomCharacter()); + } + } + } + + private draw(): void { + if (!this.rainCanvas) { + return; + } + + const canvas = this.rainCanvas.nativeElement; + const context = this.context; + if (!context) { + return; + } + + const width = canvas.clientWidth; + const height = canvas.clientHeight; + const fontSize = Math.max(14, Math.min(20, width / 58)); + context.clearRect(0, 0, width, height); + context.font = `${fontSize}px ${getComputedStyle(canvas).fontFamily}`; + context.textAlign = 'center'; + context.textBaseline = 'top'; + context.shadowBlur = 0; + + for (const column of this.columns) { + for (let index = 0; index < column.characters.length; index += 1) { + const characterY = column.y + index * fontSize; + if (characterY < -fontSize || characterY > height) { + continue; + } + + const isHead = index === 0; + const isHighlight = index === Math.floor(column.length * 0.35) && column.opacity > 0.5; + const trailAlpha = column.opacity * (1 - index / (column.length * 1.15)); + const edgeFade = Math.min(1, Math.max(0, (height - characterY) / (fontSize * 5))); + const alpha = Math.max(0, trailAlpha * edgeFade); + if (alpha === 0) { + continue; + } + context.fillStyle = isHead + ? `rgba(220, 255, 226, ${Math.min(1, alpha + 0.3)})` + : isHighlight + ? `rgba(80, 255, 126, ${alpha})` + : `rgba(18, 174, 78, ${alpha})`; + if (isHead || isHighlight) { + context.shadowColor = 'rgba(37, 255, 104, 0.8)'; + context.shadowBlur = isHead ? 9 : 5; + } + context.fillText(column.characters[index], column.x, characterY); + context.shadowBlur = 0; + } + } + } + + private randomCharacter(): string { + const characterSet = Math.random() < 0.48 + ? BINARY_CHARACTERS + : Math.random() < 0.68 + ? HEX_CHARACTERS + : Math.random() < 0.72 + ? CODE_CHARACTERS + : TERMINAL_CHARACTERS; + + return characterSet[Math.floor(Math.random() * characterSet.length)]; + } + + private randomInteger(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } } diff --git a/src/frontend/src/assets/portfolio.txt b/src/frontend/src/assets/portfolio.txt new file mode 100644 index 0000000..afc0028 --- /dev/null +++ b/src/frontend/src/assets/portfolio.txt @@ -0,0 +1,93 @@ + + ilI,; l!;;I;:l + i>:;!,,:;'.``^`'`^^";:, + ,^",` ^".."` . ."'^' ."'.^!:, + :I::,^":,``' .. .'.'.. ..''. + ~~>+_~i!i!I:^`^ ... ... .` + ~l,`"`::;+/vzXUQ00LCJUYYXzvur/|1}}{[[[[]?-+:^ . + "',,'':~i}CmmwqppwwwmZO0QCJUYYUUUUYYzvxjf/\|(||1[?_+~i" + "":``^":i_X0OmwpdqwwwqqpppmOQCLLQQQQCXcunxf/|(11{]-+~<>I^. + "^^i>i>i<~)YLQOpdqwpdbkkkkbdqwZZO0O0CXvnxxrft\\|){]-+~<<+_+)XLQO0XvYUcrt\\\rvJ0ZZZQUcj({-~>!ll!i<>i!i>><>>:^.. . + ''^li:`;:"<)YQJnt1{]<;,::;!i?1fQdpCn)?>l"^`'''`''`^"",^;>>I'... ... + ''!` ``^li[UY/rvxucxrrt|111(|fQhhLf]+?[{}?~i!l;,;il' . . + .^;l'''`,;l_YnzQOwwppOYnt|))\rJwbpLr?Iii' . + `<^'.";!i~+cYQQCzXct{i^ . . + iI"'.,i"I~_YQ0Yt[<}(;":![~/Oww0LUY0OUvrffxUwqpkkZu1-___++?[[)(1[?-+<><+--+<^ . + il:":i;!_JZqpko*hdmOO0Owbakdbad0Xf{-+_--[{)(|\||()1}[?__+~!. + !,;!:!i\Omdkooahkkbkkka*hbh#MpQJz\[-++-[1(/fxnnnxj/(}]_+~~, . + "^!,!+!umpkho*#*oaahkkdkhhoMowCct}]?--_?{(tjnvcvux/(1}]_++l + ?[~:<>lUpbkh*#MW#ohdZUYwqJnCQx|{-~l-Opbdho##*akmUt/Cdpn~]f{_~il:,lii+--?}|/tt\()1}]??-+>" ,;I< + /U_~+_---]}1)11}[]]--_~>l' ll:,~ + u\~|k&Z~[QZwwppqwZQcuJJOdpbbdbqv)(//{__~<~~+~~~?][}[]]--__+<>i^."I_-][]?-----+~_:i + vwpcvQpJCZwpqmmmmQn{)txfuXLOQJXvcnjj\]~il;IIl+[{[]?--?-_+~<~_~<>i+ + rZboahQLOmwmmZOOZwdkkbddpOJuf|))\|1[]??-?]]][]------_~<i + tzJQJU0OmmmZZmwwqdkkkkkbdqZQCYzvj\1}[?-??]]?---___+~~!!l^ + 1nXJQOZmwpdbkhaddpm0UXcut)[[[]?---___++<><<>!I;:," + f\)|/rvvzCCXuxf/(}-__<~>i!!!!i!!!l!l;:,:,;; + rCj/{{{1(|\({[-_++_<~~<<~~>il!lII;;Ill!iil + 0mOQUYzczzuf([-+ii!lIIlI;Il!!iiiI-/ + C0OO0QLCQQLJJJXnf({?+~<<>ii!!l!!!><>i!!+)uz| + *&UOOZZO000QQLCCJXvx([_~~~<<<>>>iiii~?(rczvur + f$@aOOmmmmZOOQQLLCCUj}?_+~+_~~>i<+])fuzXXzvvnn(l + Z@@$%hwmwwmZO00QLJYx1[[[[]-++])jvXYUYXzccvvvnxr< ^i + )8$@@$$B*dwmOZZO0LUvt()1{1\xUOwwOCUYYXzzccvvunnx)`'." + [-C$@$$$@$$$8aqZOOO0CzurrvCd#8&#kqZ0QCYXccccvvvuxr-'''.',i + }}}M$@$$$$$@@$$$B#dZCCLZbW$$$$8MokpwZLYzccccvvvunj),'''''..^l + -]{+r$@$$$$@@$$@%WodOwbOQJXXYJ0wddkhkq0JXzzzccvvxxrf+`^`'''''...':i + --_?}]~L$B$@@$$$8a#dLYYJk#dmCXcccvu+~|nYLO0CUXzzccvvvu|I"^```````'`'...'^":li + ]]??--]]]>w$B@$$Bbc(1dW8@$$@B$$BMdJxf)(f)?_]|xUQOZ0QQLJQj!;,"^^^^^^`'...........''^,;! + _+~<<~_??-]]?->w$$$#0r|{{tzmbkkoM%@$BWbCr)[[][}}[-<<_{tvU0wOc~~l,""^^""^^",^`'`^^^^^^^^``'''^":Ii + _+~~~<~~ + +++~~~~++__---?]]???]?<1zm%phB%*oW%BB&k0XJYb$$*X\1[11_~]{|rcJ0wd*Wkz(tc_iI:,,",:::,:;l!lI;:^.'":::::;;:;:;:::,,""",:;l!> + +++~~~+++++--__----?]????]_itJo@$$$B&M&$$$@BWk0xm&aCf)}{1{)\fjuU0mdoW&hJjX*Y>>l;:,",,:;IlIlii!;,. ^:;;IIIIIII;II;;;;;:::,,,,:,,:;;! + ~~~~~++___++_++?---_---]]????-~<}|1fttftvQwk#&WkCuc*#[!:' ^;IIIllllllllllIIlIII;I;;;;;:::":ll;": + ~<<~~+__++++_____+_]__-_-??]????]->-rL8$$$$@B%8B@B%8&%$*d$L1_i!{tj/\\nLwk#&8h0YXO&z>>iI^ ':Illl!!!!!l!!!!l!!lllllllIII;;:!>l:`'..! + <<~<~+++_-________-__++_]__-_-?-?]??]?-i{xJ&$@$$$$@B@%8MW&B$ka%Y)-?+?r//xUOph#&WawCJZhb-<iii!!iii!!!!!!!!!llll:i>I:^'''.. + ~><<><~+_-_+-_____---_-__+_~_]_-?--?-?]??]-+!)fL8$@$$$$@BB&WMW8$Bd%&Y(?_]?|tcZdkoMWMapOLOdWr>>lI;::,:;II!i<<>l:. ^Iliiii>>>>ii!ii!iiii!!ii!llll;i<;,^....'." + +~;i++___---_-??_-_---_--__++~_?--?-?--]]?]?-~!)tO%$$$$@@B%%8%WW%@*M@hY/[---]f0dh*WW#*km0Qq*w~~iI;:,,:;;II!><<<>>>i!!i!!iiiii!ill!l!I!<;,`'....''. + _-i:------_----??--_-_-___-__+_-?--?????]]]??_>i!" .:liii>>>>~ilI;::::Ill!ii>!:. "!i><><<>>>i!lIIIll!!ii!!lII!l:~l"^''.......'. + ___~"+-____---__-??---__~_-__?---]---?-??]]]?-_><)j*$@$@B&Woka#8@$@@@8aOXnf\)){{{JM8&#obOQmhk]~>!I;:,::;I!!!i>>I' `li>><<<;,^'........'.: + -__+,l?______-_???-?-_+__------__]?--???]]]]?-_i+)c%$@@B8#ahaWB@$@@@%Wk0Ycxf\\1{{xo%8MhmC0dWn~!" 'I>><>><i: 'I>>>>>>iii!lIII;:,:;IllllI:"""`<>:`''.......'...' + _?_++<^!-+_+_---??---__---???-?___?]--?]]??]]]?_!>1p$@@BWabk#%@@@@B%W*apQCYcuxr|[{tkB8hZLZb#)<>lI;:::;IIl!iiii;..;>>i>>>!!i!I;;lI::,,;IIll;:,^^.;+l,`'''''.....''. ^ + _-_~~)qBBB8*kk*8@@B%%&M*aodOLCYcux|[[/hB&bZ0qWX>>>l' ,!ii>>!ii!llI;,:;;;,,:;I;::,"^'.>i;,^^`''''.....''. ; + _-_<~>!^l____?---_--___-_?-----_-_-]?-???]]]??--<~(0&BBWhko&@@B%&M*hhaadmO0JXcv|?[r*%#bZ0hd_~!I;:,::IIII!>ii!` ^l!!i!l!!lllI;:::,::::;;;::"^^' "~l;,^``'''.......''.' + __+~<>iI,~+_---__-_-___??---?---___]]-?]]???????~~|za8%Mkk#B$B%W*kkddkhdmwZQUXc)?[uW%#bZZo|il^ `:li!IIll;llIIl;"`^",:;;;:"^^`'' l~I;,`''''.........'' + ~__~<><>!"l+__-__--____-??_--_----__]??-??-???]??>;)fmM8#ka&@B%MaadppkaakpqqZLCz1?[c&%opZhu<>I;:,,:;IIIlli>l` ^!!i!lIl;::;:::""^^^"""::"^'`'''..ii;:,`'''...........' , + ~~~<>i>>i:^<+__+__-__-_---?-_---_-__-]?----???]?_i"]|Jo&*a*&@B&ddbbpb*##hdbdqm0z[]}X&&kwaY<iiiil^l_+++_+__---?----_--_-_-__]?----??---+>,<(ck##*#8@%amwOppd#&Whbbddqwc]}}C%#wkQ<>!I:,"":;IIIliil` ',ll!!llII;;:;:,"^"""""^'..''..... ,I;"`'''...............^ + ~~<<!l;:""":;Il!i!I` .":;;I;;;;;;;:,,"^^"^""^`'..''..... ,;:^^''''............... + <<<:+\Ch#W&B&#admmmOdW8&MM&Wokkn?}1wWbbr>>lI;,,,,:;I!!!;' '":;;;II;;;I:::,""^^^```''...''.... .":"^`''''`'''.........'.` + ><<ii!!!!!il;^,+~~~+++~~_-__-_-_-__-___+-?_-__-__-_~i"_\Jh#WMbWdpwOqbaW8&MW&#*ohf_[fmh0+>!l;:,,::;IIlI,..'",:;;;;;;:::::,""""^^``''.......... '`^^^^^^^^````'.'.........` + <<<>!!!!ll!!!l:'I~<~++++_+++_+----_-___--_?-______-+<>;;1rq*#hd&bqwmpaaW8WMMM*oak\~[jwp]>ilI::,,:;;III"..'":::;;::::::,,""""^^^`''''......... .`^^^^^^``````'............ + >>!^_/Chopb&ahkbh*M&WW###ohab)>-nd1i>lI;:::::;II;^ .'^,,::::;:::,:::,",""`'''''.......... .'^^""^^````^^`'...........^ + i><>i!!llllll;I;,"."+<<~+~~+_+____+_~_+__~++<--++~++_~l:,{rJzkM####MMM##ohhbddkb1!]~>ilI;:;IlIII". .'^,;I;;;Il;lII;;,,^^'''.............. .'`^^`^``^`'^`^`''........... + <<>__~<~~~<!l;;;;;IlI;^...'"::;II;lIIII;;,""^''................. ..'```'''````````''...'......` + >>!ll!lllIlIIII:,^.. ^<~~<~+~++~~+++++~++<++<>>-_~<<<>>il;^]/jpaaaaoao*oahakmZO0w)~illIllll;;lI;;:"`. '<~<~+<~~<~~<~~~+~~+<~~ii!I"I}(LqdbkkkhhhhkhkmQJJY_>i!IIIIll!!;' ..^,;;Ill!!lI;:,^`''..................... .''```^^^```^^``'''..''......, + >!Illl!lII;I;;;,^`' ^+~~~~<~<<<<<<<+~~<~~~~<<>+~i!!ii!lI"!]uYLOOmpkhhhhkpOJYX?ii!!;l!!!!l,. ..^,;Il!lllI:,^^'''....................... .'````^^^^```^'`'''....'.....' + illllllIIII;;I;"^`' ;_+~<<~<<<<<<~<~~<<+<>~_+i!iii!l;^,1xcYJQmkhhkkbp0UY]ii!!lli>i!l^ .'^,;;llII;:,"^``'''...................... ..'```^^^`^^^^```'''....'..... + illlll;;II;;;;;,^`'. !_<>~~~+><<<<<~<<<><<><>>>~+<<+~~~>;`l|uvvcCwbhkbdw0L1iil!!!i>i!;' ..'^,;;;I;;;,""``''......................... ..'``^^^^^^^^```''''....''....^ + lIlllII;I;;;:,:"^`'. .<_<<<~+<<<<<+~<>><<<<>>i!:. ..'^,;:;;;:""^`'''.......................... .''``^^^^""^``'''''.....'..... + illllI;;;;;:;:,:"^`'. `_~>><<>><>~~<<><<<<~<><>i!i+!iii>>il^ ..`":;;:;,,^^`'''............................ .''``^^^^""^``'''''......'... , \ No newline at end of file diff --git a/src/frontend/tsconfig.app.json b/src/frontend/tsconfig.app.json index 264f459..e4dd97c 100644 --- a/src/frontend/tsconfig.app.json +++ b/src/frontend/tsconfig.app.json @@ -4,6 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", + "rootDir": "./src", "types": [] }, "include": [