added websitedna
This commit is contained in:
@@ -1,84 +1,56 @@
|
||||
# 🌊 Network Aquarium
|
||||
# 🌊 VrbaOne Projects · .NET 10 Ecosystem
|
||||
|
||||
> **Živý network traffic vizualizovaný ako podmorské akvárium v .NET 10.**
|
||||
> Každý request, stream a chyba v tvojej infraštruktúre ožíva ako morský tvor s nulovou záťažou na produkčný server.
|
||||
V tomto repozitári sa nachádzajú dva samostatné projekty postavené na **.NET 10** a **Blazor WebAssembly**:
|
||||
|
||||
1. **🌊 Network Aquarium** – Živý server traffic vizualizovaný ako interaktívne podmorské akvárium.
|
||||
2. **🧬 Website DNA** – Generatívny digitálny fingerprint a genóm ľubovoľného webu.
|
||||
|
||||
---
|
||||
|
||||
## 🐠 Morská hierarchia (Traffic Taxonomy)
|
||||
## 1. 🌊 Network Aquarium
|
||||
|
||||
| Tvor | Sieťový traffic | Vizuál a fyzika |
|
||||
| :--- | :--- | :--- |
|
||||
| 🐟 **Rybky (Klauni, Tetry, Skaláre)** | Štandardný HTTP GET / POST / PUT / DELETE | Farba podľa metódy (GET = Cyan, POST = Emerald, PUT = Violet, DELETE = Rose). Rýchlosť plávania = Latencia (nízky ping = blesková rybka). |
|
||||
| 🐬 **Delfíny** | WebSockets / SSE / Live Streamy | Plávajú v sínusoidách, skáču k hladine a pri každom data frame emitujú sonarové vlny. |
|
||||
| 🐋 **Veľryby** | Veľké downloady & payloady (> 500 KB – 10 MB+) | Majestátne, obrovské, pomalé tvory s vodným gejzírom z dýchacieho otvoru. |
|
||||
| 🪼 **Medúzy (Červené / Jantárové)** | Chyby klienta (4xx) & Chyby servera (5xx) | Svietiace pulzujúce medúzy vznášajúce sa nahor s dlhými chápadlami. |
|
||||
| 🦈 **Žraloky** | Rate Limits (429) & DDoS / Traffic Spikes | Rýchle dravce križujúce stredné hĺbky pri preťažení servera. |
|
||||
| 🦀 **Krabíci** | Databázové dotazy & Background Cron / Workers | Sliedia po piesočnatom dne a prenášajú dáta. |
|
||||
Živý network traffic vizualizovaný ako akvárium s nulovou záťažou na server.
|
||||
|
||||
---
|
||||
* **🐟 Rybky:** Štandardné HTTP GET/POST/PUT/DELETE requesty (rýchlosť = latencia).
|
||||
* **🐬 Delfíny:** WebSockets a streamy s animovanými sonarovými vlnami.
|
||||
* **🐋 Veľryby:** Veľké downloady a payloady (> 500 KB – 10 MB+).
|
||||
* **🪼 Medúzy:** 500 chyby servera (červené) a 4xx chyby klienta (jantárové).
|
||||
* **🦈 Žraloky:** 429 Rate-Limits & DDoS traffic spiky.
|
||||
* **🦀 Krabíci:** Databázové operácie a background worker úlohy.
|
||||
|
||||
## 🏛️ Architektúra (.NET 10 Solution)
|
||||
|
||||
Riešenie `VrbaOneProjects.slnx` obsahuje 3 projekty:
|
||||
|
||||
1. **`Aquarium.Shared`** (.NET 10 Class Library)
|
||||
- Modely `TrafficEvent`, `CreatureSpawnDto`, `AquariumStats`.
|
||||
- `CreatureClassifier` – inteligentné pravidlá mapovania trafficu na biómy, rýchlosť, škálovanie a farby.
|
||||
2. **`Aquarium.Server`** (.NET 10 ASP.NET Core Minimal API + SignalR)
|
||||
- `TrafficTelemetryMiddleware` – zachytáva live HTTP požiadavky bez blokovania.
|
||||
- `ITrafficEventChannel` – in-memory `System.Threading.Channels` fronta (zero-allocation, mikrosekundový throughput).
|
||||
- `TrafficBroadcasterService` – agreguje RPS, latenciu a vysiela eventy cez SignalR do pripojených klientov.
|
||||
- `TrafficSimulatorService` – generuje realistický traffic pre testovanie a demo účely.
|
||||
3. **`Aquarium.Client`** (.NET 10 Blazor WebAssembly Standalone)
|
||||
- 60 FPS HTML5 Canvas 2D engine (`aquarium.js`) s realistickými časticami, lomom svetla (caustics) a morskými prúdmi.
|
||||
- **Click-to-Inspect HUD:** Kliknutím na akéhokoľvek tvora sa otvorí detailný cyberpunkový drawer s metrikami (`Method`, `Path`, `Status`, `Latency`, `Size`, `Client IP/Country`, `Trace ID`).
|
||||
- Syntetizované audio efekty (Web Audio API) s možnosťou stlmenia.
|
||||
- Offline / Standalone Demo Fallback – ak backend nebeží, klient sa automaticky prepne do lokálnej simulácie.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Spustenie projektu
|
||||
|
||||
### 1. Spustenie Backend Servera (Port 5050)
|
||||
### Spustenie Network Aquarium:
|
||||
```powershell
|
||||
cd src/Aquarium.Server
|
||||
dotnet run --urls "http://localhost:5050"
|
||||
```
|
||||
# Backend (Port 5050)
|
||||
dotnet run --project src/Aquarium.Server --urls "http://localhost:5050"
|
||||
|
||||
### 2. Spustenie Blazor WASM Klienta (Port 5100)
|
||||
```powershell
|
||||
cd src/Aquarium.Client
|
||||
dotnet run --urls "http://localhost:5100"
|
||||
# Frontend Blazor WASM (Port 5100)
|
||||
dotnet run --project src/Aquarium.Client --urls "http://localhost:5100"
|
||||
```
|
||||
|
||||
Otvorte prehliadač na **`http://localhost:5100`**.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Ako napojiť reálny server
|
||||
## 2. 🧬 Website DNA
|
||||
|
||||
Máte 2 jednoduché spôsoby:
|
||||
Zadajte akékoľvek URL a získajte vizuálny fingerprint a genóm webu.
|
||||
|
||||
### Možnosť A: Vloženie Middleware do existujúcej ASP.NET Core aplikácie
|
||||
Pridajte referenciu na `Aquarium.Server` alebo vložte `TrafficTelemetryMiddleware`:
|
||||
```csharp
|
||||
app.UseMiddleware<TrafficTelemetryMiddleware>();
|
||||
```
|
||||
* **3D Rotujúca Double Helix:**
|
||||
- **A (Modrá):** Sémantické HTML a štruktúra
|
||||
- **T (Fialová):** CSS a štylistická hustota
|
||||
- **C (Žltá):** JavaScript a interakčná logika
|
||||
- **G (Zelená):** Mediá, obrázky, SVG a fonty
|
||||
* **🎨 Chromozomálne farebné pásy:** Extrahované dominantné farby z CSS palety stránky tvoria jadro DNA lúča.
|
||||
* **☣️ Mutačné anomálie:** Detekcia trackerov (Google Analytics, Meta Pixel, Hotjar, Sentry) ako žiariace spóry.
|
||||
* **⚔️ Cross-Comparison Mode:** Porovnanie dvoch webov vedľa seba (napr. Google vs Reddit) s výpočtom genetickej kompatibility.
|
||||
* **🎵 DNA Sonifikácia:** Web Audio API generuje ambientné hudobné akordy z báz A-T-C-G.
|
||||
* **🎴 Hologram Card Export:** Export do PNG zberateľskej karty na 1 klik.
|
||||
|
||||
### Možnosť B: Externé posielanie cez REST Ingestion API
|
||||
Akákoľvek reverzná proxy (Nginx, Caddy, Cloudflare Worker, Node.js) môže poslať event priamo na endpoint servera:
|
||||
```bash
|
||||
curl -X POST http://localhost:5050/api/telemetry/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/users",
|
||||
"statusCode": 200,
|
||||
"durationMs": 42.5,
|
||||
"bytes": 12400,
|
||||
"clientCountry": "SK",
|
||||
"clientCity": "Bratislava",
|
||||
"clientIp": "178.40.12.8"
|
||||
}'
|
||||
### Spustenie Website DNA:
|
||||
```powershell
|
||||
# Backend (Port 5060)
|
||||
dotnet run --project src/WebsiteDna.Server --urls "http://localhost:5060"
|
||||
|
||||
# Frontend Blazor WASM (Port 5160)
|
||||
dotnet run --project src/WebsiteDna.Client --urls "http://localhost:5160"
|
||||
```
|
||||
Otvorte prehliadač na **`http://localhost:5160`**.
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
<Project Path="Aquarium.Client/Aquarium.Client.csproj" />
|
||||
<Project Path="Aquarium.Server/Aquarium.Server.csproj" />
|
||||
<Project Path="Aquarium.Shared/Aquarium.Shared.csproj" />
|
||||
<Project Path="WebsiteDna.Client/WebsiteDna.Client.csproj" />
|
||||
<Project Path="WebsiteDna.Server/WebsiteDna.Server.csproj" />
|
||||
<Project Path="WebsiteDna.Shared/WebsiteDna.Shared.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,5 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="dna-app-container">
|
||||
@Body
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">WebsiteDna.Client</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
@page "/"
|
||||
@inject DnaStateService Dna
|
||||
@inject IJSRuntime JS
|
||||
@implements IDisposable
|
||||
|
||||
<div class="dna-app-container">
|
||||
<!-- Top Cyberpunk Scanner Bar -->
|
||||
<header class="dna-header">
|
||||
<div class="brand">
|
||||
<span class="brand-icon">🧬</span>
|
||||
<div>
|
||||
<h1 class="brand-title">WEBSITE DNA</h1>
|
||||
<p class="brand-tagline">Genetic Fingerprint of the Web · .NET 10 WASM</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="url-input-wrap">
|
||||
<input type="text" class="url-input" placeholder="Enter website URL (e.g. stripe.com)..."
|
||||
@bind="_inputUrl" @bind:event="oninput" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<button class="btn-sequence" @onclick="AnalyzeEnteredUrl" disabled="@Dna.IsLoading">
|
||||
@if (Dna.IsLoading)
|
||||
{
|
||||
<span>⏳ SEQUENCING...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>⚡ SEQUENCE</span>
|
||||
}
|
||||
</button>
|
||||
<button class="btn-compare-toggle" @onclick="OpenCompareModal">
|
||||
⚔️ COMPARE
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Preset Pills Bar -->
|
||||
<div class="presets-bar">
|
||||
<span class="preset-label">Genomic Presets:</span>
|
||||
@foreach (var preset in Dna.Presets)
|
||||
{
|
||||
var isAct = Dna.CurrentProfile?.Domain == preset.Domain;
|
||||
<button class="preset-pill @(isAct ? "active" : "")" @onclick="() => SelectPreset(preset)">
|
||||
@preset.Domain
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Main Workspace -->
|
||||
<div class="main-workspace">
|
||||
<!-- Interactive 2D/3D Canvas Viewport -->
|
||||
<div class="canvas-viewport">
|
||||
<canvas id="dnaCanvas"></canvas>
|
||||
|
||||
<!-- Floating Viewport Controls -->
|
||||
<div class="viewport-controls">
|
||||
<button class="btn-ctrl @(Dna.CurrentRenderMode == "helix" ? "active" : "")" @onclick='() => SetMode("helix")'>
|
||||
🌀 3D Double Helix
|
||||
</button>
|
||||
<button class="btn-ctrl @(Dna.CurrentRenderMode == "radial" ? "active" : "")" @onclick='() => SetMode("radial")'>
|
||||
🧬 Radial Mandala
|
||||
</button>
|
||||
<button class="btn-ctrl" @onclick="ToggleAudio">
|
||||
@(Dna.SoundEnabled ? "🔊 Sound ON" : "🔇 Sound OFF")
|
||||
</button>
|
||||
<button class="btn-ctrl" @onclick="ExportCard" title="Download Hologram Card PNG">
|
||||
🎴 Export Card
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organism Hologram Dossier (Right Side Panel) -->
|
||||
<aside class="dossier-panel glass-panel">
|
||||
@if (Dna.CurrentProfile != null)
|
||||
{
|
||||
var p = Dna.CurrentProfile;
|
||||
<div class="organism-header">
|
||||
<div>
|
||||
<div class="organism-name">@p.Domain</div>
|
||||
<div class="organism-domain">@p.Title</div>
|
||||
</div>
|
||||
<span class="rarity-badge @p.Rarity">@p.Rarity</span>
|
||||
</div>
|
||||
|
||||
<div class="archetype-tag">
|
||||
<span>🧬 Archetype:</span>
|
||||
<strong style="color: #38bdf8;">@FormatArchetype(p.Archetype)</strong>
|
||||
</div>
|
||||
|
||||
<!-- Genetic Scores Trio -->
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Purity</div>
|
||||
<div class="metric-num purity">@p.PurityScore%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Complexity</div>
|
||||
<div class="metric-num complexity">@p.ComplexityScore</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">Mutagens</div>
|
||||
<div class="metric-num mutation">@p.MutationScore%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Genetic Summary Callout -->
|
||||
<div class="summary-box">
|
||||
@p.GeneticSummary
|
||||
</div>
|
||||
|
||||
<!-- Chromosome Palette Band -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">🎨 Chromosome Palette</div>
|
||||
<div class="palette-strip">
|
||||
@foreach (var band in p.ColorBands)
|
||||
{
|
||||
<div class="palette-item" style="width: @band.Percentage%; background-color: @band.Hex;" title="@band.Role (@band.Hex)"></div>
|
||||
}
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-size: 0.72rem; color: #94a3b8; font-family: 'JetBrains Mono', monospace;">
|
||||
@foreach (var band in p.ColorBands)
|
||||
{
|
||||
<span>@band.Hex</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nucleotide Base Distribution (A-T-C-G) -->
|
||||
<div class="detail-card base-pair-section">
|
||||
<div class="card-title">🧬 Nucleotide Base Distribution</div>
|
||||
|
||||
<div class="base-pair-row">
|
||||
<span class="base-label" style="color: #38bdf8;"><span>A</span> <small style="font-size:0.68rem; color:#94a3b8;">HTML/Structure</small></span>
|
||||
<div class="base-bar-wrap">
|
||||
<div class="base-bar-fill A" style="width: @(GetBasePercent(p, "A"))%;"></div>
|
||||
</div>
|
||||
<span style="font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">@(GetBasePercent(p, "A"))%</span>
|
||||
</div>
|
||||
|
||||
<div class="base-pair-row">
|
||||
<span class="base-label" style="color: #c084fc;"><span>T</span> <small style="font-size:0.68rem; color:#94a3b8;">CSS/Styles</small></span>
|
||||
<div class="base-bar-wrap">
|
||||
<div class="base-bar-fill T" style="width: @(GetBasePercent(p, "T"))%;"></div>
|
||||
</div>
|
||||
<span style="font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">@(GetBasePercent(p, "T"))%</span>
|
||||
</div>
|
||||
|
||||
<div class="base-pair-row">
|
||||
<span class="base-label" style="color: #fde047;"><span>C</span> <small style="font-size:0.68rem; color:#94a3b8;">JS/Logic</small></span>
|
||||
<div class="base-bar-wrap">
|
||||
<div class="base-bar-fill C" style="width: @(GetBasePercent(p, "C"))%;"></div>
|
||||
</div>
|
||||
<span style="font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">@(GetBasePercent(p, "C"))%</span>
|
||||
</div>
|
||||
|
||||
<div class="base-pair-row">
|
||||
<span class="base-label" style="color: #34d399;"><span>G</span> <small style="font-size:0.68rem; color:#94a3b8;">Media/Assets</small></span>
|
||||
<div class="base-bar-wrap">
|
||||
<div class="base-bar-fill G" style="width: @(GetBasePercent(p, "G"))%;"></div>
|
||||
</div>
|
||||
<span style="font-family: 'JetBrains Mono', monospace; font-size: 0.75rem;">@(GetBasePercent(p, "G"))%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DOM Architecture & Biomass -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">🏛️ DOM & Biomass Metrics</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 0.78rem;">
|
||||
<div><span style="color: #94a3b8;">DOM Nodes:</span> <strong style="font-family: 'JetBrains Mono', monospace;">@p.DomMetrics.TotalNodes</strong></div>
|
||||
<div><span style="color: #94a3b8;">Max Depth:</span> <strong style="font-family: 'JetBrains Mono', monospace;">@p.DomMetrics.MaxDepth lvl</strong></div>
|
||||
<div><span style="color: #94a3b8;">Semantic Ratio:</span> <strong style="font-family: 'JetBrains Mono', monospace;">@((int)(p.DomMetrics.SemanticRatio * 100))%</strong></div>
|
||||
<div><span style="color: #94a3b8;">Total Biomass:</span> <strong style="font-family: 'JetBrains Mono', monospace;">@FormatBytes(p.AssetFootprint.TotalBytes)</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detected Trackers & Mutagens -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">⚠️ Detected Mutagens & Trackers</div>
|
||||
@if (p.DetectedTrackers.Count > 0)
|
||||
{
|
||||
<div class="tag-list">
|
||||
@foreach (var tracker in p.DetectedTrackers)
|
||||
{
|
||||
<span class="tracker-tag">☣️ @tracker</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="font-size: 0.78rem; color: #34d399; font-family: 'JetBrains Mono', monospace;">
|
||||
✓ Zero tracking mutations detected (Pure Genome)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Comparison Modal -->
|
||||
@if (_showCompareModal)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content glass-panel">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 12px;">
|
||||
<h2 style="font-family: 'Orbitron', sans-serif; font-size: 1.1rem; color: #c084fc;">⚔️ GENOMIC CROSS-COMPARISON</h2>
|
||||
<button style="background:none; border:none; color:#94a3b8; font-size:1.4rem; cursor:pointer;" @onclick="CloseCompareModal">×</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 16px;">
|
||||
<select class="url-input" style="flex:1;" @bind="_compareUrl1">
|
||||
@foreach (var preset in Dna.Presets)
|
||||
{
|
||||
<option value="@preset.Domain">@preset.Domain (@preset.Archetype)</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<span style="align-self: center; font-family: 'Orbitron', sans-serif; font-weight: 800; color: #c084fc;">VS</span>
|
||||
|
||||
<select class="url-input" style="flex:1;" @bind="_compareUrl2">
|
||||
@foreach (var preset in Dna.Presets)
|
||||
{
|
||||
<option value="@preset.Domain">@preset.Domain (@preset.Archetype)</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<button class="btn-sequence" @onclick="RunComparison">
|
||||
RUN COMPARISON
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (Dna.ActiveComparison != null)
|
||||
{
|
||||
var comp = Dna.ActiveComparison;
|
||||
<div style="margin-top: 20px; padding: 16px; background: rgba(168, 85, 247, 0.15); border: 1px solid rgba(168, 85, 247, 0.4); border-radius: 8px;">
|
||||
<div style="font-size: 0.95rem; font-weight: 700; color: #e9d5ff;">@comp.CompatibilityVerdict</div>
|
||||
|
||||
<div class="compare-grid">
|
||||
<div class="detail-card">
|
||||
<h3 style="font-family: 'Orbitron', sans-serif; font-size: 0.9rem; color: #38bdf8;">@comp.ProfileA.Domain</h3>
|
||||
<div style="margin-top: 6px; font-size: 0.8rem;">
|
||||
<div>Archetype: <strong>@FormatArchetype(comp.ProfileA.Archetype)</strong></div>
|
||||
<div>Purity: <strong>@comp.ProfileA.PurityScore%</strong></div>
|
||||
<div>Biomass: <strong>@FormatBytes(comp.ProfileA.AssetFootprint.TotalBytes)</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<h3 style="font-family: 'Orbitron', sans-serif; font-size: 0.9rem; color: #c084fc;">@comp.ProfileB.Domain</h3>
|
||||
<div style="margin-top: 6px; font-size: 0.8rem;">
|
||||
<div>Archetype: <strong>@FormatArchetype(comp.ProfileB.Archetype)</strong></div>
|
||||
<div>Purity: <strong>@comp.ProfileB.PurityScore%</strong></div>
|
||||
<div>Biomass: <strong>@FormatBytes(comp.ProfileB.AssetFootprint.TotalBytes)</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _inputUrl = "";
|
||||
private bool _showCompareModal = false;
|
||||
private string _compareUrl1 = "google.com";
|
||||
private string _compareUrl2 = "reddit.com";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Dna.OnStateChanged += StateHasChanged;
|
||||
await Dna.InitializeAsync("dnaCanvas");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
{
|
||||
await AnalyzeEnteredUrl();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AnalyzeEnteredUrl()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_inputUrl))
|
||||
{
|
||||
await Dna.AnalyzeUrlAsync(_inputUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectPreset(WebsiteDnaProfile preset)
|
||||
{
|
||||
_inputUrl = preset.Domain;
|
||||
await Dna.SelectProfileAsync(preset);
|
||||
}
|
||||
|
||||
private async Task SetMode(string mode)
|
||||
{
|
||||
await Dna.SetRenderModeAsync(mode);
|
||||
}
|
||||
|
||||
private async Task ToggleAudio()
|
||||
{
|
||||
await Dna.ToggleSoundAsync();
|
||||
}
|
||||
|
||||
private async Task ExportCard()
|
||||
{
|
||||
await Dna.ExportCardAsync();
|
||||
}
|
||||
|
||||
private void OpenCompareModal()
|
||||
{
|
||||
_showCompareModal = true;
|
||||
}
|
||||
|
||||
private void CloseCompareModal()
|
||||
{
|
||||
_showCompareModal = false;
|
||||
Dna.CloseComparison();
|
||||
}
|
||||
|
||||
private async Task RunComparison()
|
||||
{
|
||||
await Dna.CompareAsync(_compareUrl1, _compareUrl2);
|
||||
}
|
||||
|
||||
private int GetBasePercent(WebsiteDnaProfile p, string baseLetter)
|
||||
{
|
||||
var total = p.BaseCounts.Values.Sum();
|
||||
if (total == 0) return 25;
|
||||
var count = p.BaseCounts.GetValueOrDefault(baseLetter, 0);
|
||||
return (int)Math.Round((double)count / total * 100);
|
||||
}
|
||||
|
||||
private string FormatArchetype(OrganismArchetype arch) => arch switch
|
||||
{
|
||||
OrganismArchetype.MinimalistCrystal => "Minimalist Crystal 💎",
|
||||
OrganismArchetype.TextualCoral => "Textual Coral 🪸",
|
||||
OrganismArchetype.NeuralHyperWeb => "Neural Hyper-Web 🕸️",
|
||||
OrganismArchetype.MediaLeviathan => "Media Leviathan 🐋",
|
||||
OrganismArchetype.DesignSynthesizer => "Design Synthesizer ✨",
|
||||
OrganismArchetype.BloatedChimera => "Bloated Chimera ☣️",
|
||||
_ => arch.ToString()
|
||||
};
|
||||
|
||||
private string FormatBytes(long bytes)
|
||||
{
|
||||
string[] sizes = ["B", "KB", "MB", "GB"];
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len /= 1024;
|
||||
}
|
||||
return $"{len:0.#} {sizes[order]}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dna.OnStateChanged -= StateHasChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using WebsiteDna.Client;
|
||||
using WebsiteDna.Client.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<DnaStateService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5160",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7160;http://localhost:5160",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.JSInterop;
|
||||
using WebsiteDna.Shared;
|
||||
|
||||
namespace WebsiteDna.Client.Services;
|
||||
|
||||
public class DnaStateService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public string BackendUrl { get; set; } = "http://localhost:5060";
|
||||
public WebsiteDnaProfile? CurrentProfile { get; private set; }
|
||||
public List<WebsiteDnaProfile> Presets { get; private set; } = [];
|
||||
public ComparisonResult? ActiveComparison { get; private set; }
|
||||
|
||||
public bool IsLoading { get; private set; } = false;
|
||||
public string CurrentRenderMode { get; private set; } = "helix"; // "helix" | "radial"
|
||||
public bool SoundEnabled { get; private set; } = false;
|
||||
public string ErrorMessage { get; private set; } = string.Empty;
|
||||
|
||||
public event Action? OnStateChanged;
|
||||
|
||||
public DnaStateService(IJSRuntime js, HttpClient http)
|
||||
{
|
||||
_js = js;
|
||||
_http = http;
|
||||
Presets = WebsiteDnaPresets.GetPresets();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(string canvasId)
|
||||
{
|
||||
await _js.InvokeVoidAsync("DnaEngine.init", canvasId);
|
||||
|
||||
// Select Stripe or Google as initial profile
|
||||
var initial = Presets.FirstOrDefault(p => p.Domain == "stripe.com") ?? Presets[0];
|
||||
await SelectProfileAsync(initial);
|
||||
}
|
||||
|
||||
public async Task SelectProfileAsync(WebsiteDnaProfile profile)
|
||||
{
|
||||
CurrentProfile = profile;
|
||||
ErrorMessage = string.Empty;
|
||||
await _js.InvokeVoidAsync("DnaEngine.setProfile", profile);
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task AnalyzeUrlAsync(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return;
|
||||
|
||||
IsLoading = true;
|
||||
ErrorMessage = string.Empty;
|
||||
OnStateChanged?.Invoke();
|
||||
|
||||
try
|
||||
{
|
||||
var clean = url.Trim().ToLowerInvariant().Replace("https://", "").Replace("http://", "").TrimEnd('/');
|
||||
var existingPreset = Presets.FirstOrDefault(p => p.Domain.Equals(clean, StringComparison.OrdinalIgnoreCase) || p.Url.Contains(clean));
|
||||
|
||||
if (existingPreset != null)
|
||||
{
|
||||
await SelectProfileAsync(existingPreset);
|
||||
IsLoading = false;
|
||||
OnStateChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"{BackendUrl}/api/dna/analyze", new AnalysisRequest { Url = url });
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var profile = await response.Content.ReadFromJsonAsync<WebsiteDnaProfile>();
|
||||
if (profile != null)
|
||||
{
|
||||
await SelectProfileAsync(profile);
|
||||
IsLoading = false;
|
||||
OnStateChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to synthetic offline generation if server is unreachable
|
||||
}
|
||||
|
||||
// Offline client-side synthetic generation
|
||||
var synth = GenerateClientSyntheticProfile(url);
|
||||
await SelectProfileAsync(synth);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"Analysis failed: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CompareAsync(string url1, string url2)
|
||||
{
|
||||
IsLoading = true;
|
||||
OnStateChanged?.Invoke();
|
||||
|
||||
try
|
||||
{
|
||||
var p1 = Presets.FirstOrDefault(p => p.Domain.Equals(url1, StringComparison.OrdinalIgnoreCase)) ?? GenerateClientSyntheticProfile(url1);
|
||||
var p2 = Presets.FirstOrDefault(p => p.Domain.Equals(url2, StringComparison.OrdinalIgnoreCase)) ?? GenerateClientSyntheticProfile(url2);
|
||||
|
||||
int match = 0;
|
||||
int len = Math.Min(p1.GeneticSequence.Length, p2.GeneticSequence.Length);
|
||||
for (int i = 0; i < len; i++) if (p1.GeneticSequence[i] == p2.GeneticSequence[i]) match++;
|
||||
double sim = Math.Round(((double)match / Math.Max(1, len)) * 100, 1);
|
||||
|
||||
ActiveComparison = new ComparisonResult
|
||||
{
|
||||
ProfileA = p1,
|
||||
ProfileB = p2,
|
||||
GeneticSimilarityPercent = sim,
|
||||
CompatibilityVerdict = sim > 70
|
||||
? $"🧬 High Genetic Affinity ({sim}%): Both organisms share similar structural gene sequences."
|
||||
: $"⚡ Divergent Species ({sim}%): Contrasting architectural archetypes."
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseComparison()
|
||||
{
|
||||
ActiveComparison = null;
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task SetRenderModeAsync(string mode)
|
||||
{
|
||||
CurrentRenderMode = mode;
|
||||
await _js.InvokeVoidAsync("DnaEngine.setRenderMode", mode);
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task ToggleSoundAsync()
|
||||
{
|
||||
SoundEnabled = await _js.InvokeAsync<bool>("DnaEngine.toggleSound");
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task ExportCardAsync()
|
||||
{
|
||||
await _js.InvokeVoidAsync("DnaEngine.exportCardPng");
|
||||
}
|
||||
|
||||
private WebsiteDnaProfile GenerateClientSyntheticProfile(string rawUrl)
|
||||
{
|
||||
var clean = rawUrl.Trim().ToLowerInvariant().Replace("https://", "").Replace("http://", "").TrimEnd('/');
|
||||
var domain = clean.Split('/')[0];
|
||||
var seed = Math.Abs(domain.GetHashCode());
|
||||
var rnd = new Random(seed);
|
||||
|
||||
return new WebsiteDnaProfile
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Url = $"https://{domain}",
|
||||
Domain = domain,
|
||||
Title = $"{char.ToUpper(domain[0])}{domain[1..]} Organism",
|
||||
FaviconUrl = $"https://{domain}/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = (OrganismArchetype)(seed % 5),
|
||||
Rarity = (GeneticRarity)(seed % 6),
|
||||
PurityScore = Math.Round(70.0 + rnd.NextDouble() * 26.0, 1),
|
||||
ComplexityScore = Math.Round(35.0 + rnd.NextDouble() * 55.0, 1),
|
||||
MutationScore = Math.Round(5.0 + rnd.NextDouble() * 25.0, 1),
|
||||
GeneticSummary = "Procedural digital organism sequenced via client-side structural genomic hash.",
|
||||
GeneticSequence = GenerateSequence(30 + rnd.Next(20), 25 + rnd.Next(20), 20 + rnd.Next(20), 15 + rnd.Next(15), 130),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#38bdf8", Percentage = 40, Role = "Primary Helix" },
|
||||
new ColorBandDto { Hex = "#a855f7", Percentage = 30, Role = "Secondary Lattice" },
|
||||
new ColorBandDto { Hex = "#10b981", Percentage = 20, Role = "Metabolic Accent" },
|
||||
new ColorBandDto { Hex = "#f59e0b", Percentage = 10, Role = "Core Catalyst" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 44 }, { "T", 38 }, { "C", 26 }, { "G", 22 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = rnd.Next(450, 2200),
|
||||
MaxDepth = rnd.Next(10, 24),
|
||||
SemanticElements = rnd.Next(50, 180),
|
||||
DivCount = rnd.Next(200, 800),
|
||||
SpanCount = rnd.Next(100, 400),
|
||||
ScriptCount = rnd.Next(5, 20),
|
||||
StyleCount = rnd.Next(2, 8),
|
||||
ImgCount = rnd.Next(6, 30),
|
||||
SvgCount = rnd.Next(8, 40),
|
||||
ButtonCount = rnd.Next(6, 25),
|
||||
FormCount = rnd.Next(1, 3),
|
||||
SemanticRatio = 0.58
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = rnd.Next(400_000, 2_400_000),
|
||||
HtmlBytes = 75_000,
|
||||
CssBytes = 180_000,
|
||||
JsBytes = 540_000,
|
||||
ImageBytes = 380_000,
|
||||
FontBytes = 95_000
|
||||
},
|
||||
DetectedFonts = ["Inter", "system-ui", "sans-serif"],
|
||||
DetectedTrackers = ["Google Analytics 4"],
|
||||
ThirdPartyDomains = [domain]
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateSequence(int pA, int pT, int pC, int pG, int length)
|
||||
{
|
||||
var rnd = new Random(pA * 17 + pT * 11 + pC * 5 + pG);
|
||||
var chars = new char[length];
|
||||
var total = pA + pT + pC + pG;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var roll = rnd.Next(total);
|
||||
if (roll < pA) chars[i] = 'A';
|
||||
else if (roll < pA + pT) chars[i] = 'T';
|
||||
else if (roll < pA + pT + pC) chars[i] = 'C';
|
||||
else chars[i] = 'G';
|
||||
}
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.12" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WebsiteDna.Shared\WebsiteDna.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using WebsiteDna.Client
|
||||
@using WebsiteDna.Client.Layout
|
||||
@using WebsiteDna.Client.Services
|
||||
@using WebsiteDna.Shared
|
||||
@@ -0,0 +1,115 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
inset: 20vh 0 auto 0;
|
||||
margin: 0 auto 0 auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Orbitron:wght@600;800;900&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-dark: #030712;
|
||||
--bg-panel: rgba(11, 15, 25, 0.75);
|
||||
--bg-panel-border: rgba(56, 189, 248, 0.2);
|
||||
--accent-cyan: #06b6d4;
|
||||
--accent-blue: #38bdf8;
|
||||
--accent-purple: #a855f7;
|
||||
--accent-amber: #f59e0b;
|
||||
--accent-emerald: #10b981;
|
||||
--accent-rose: #f43f5e;
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body, html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-dark);
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.dna-app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(circle at 50% 20%, rgba(6, 182, 212, 0.08) 0%, rgba(3, 7, 18, 1) 70%);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: var(--bg-panel);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border: 1px solid var(--bg-panel-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Header & Scanner Bar */
|
||||
.dna-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(3, 7, 18, 0.85);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 24px;
|
||||
animation: rotateDna 12s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotateDna {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(135deg, #38bdf8 0%, #a855f7 50%, #10b981 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Search Bar & Presets */
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.url-input-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
width: 100%;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.url-input:focus {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 16px rgba(6, 182, 212, 0.35);
|
||||
}
|
||||
|
||||
.btn-sequence {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
padding: 10px 18px;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.btn-sequence:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 18px rgba(6, 182, 212, 0.4);
|
||||
}
|
||||
|
||||
.btn-compare-toggle {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
border: 1px solid rgba(168, 85, 247, 0.4);
|
||||
color: #c084fc;
|
||||
padding: 9px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-compare-toggle:hover {
|
||||
background: rgba(168, 85, 247, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Preset Pills Bar */
|
||||
.presets-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 24px;
|
||||
background: rgba(3, 7, 18, 0.6);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.preset-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.preset-pill {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.74rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-pill:hover, .preset-pill.active {
|
||||
background: rgba(56, 189, 248, 0.15);
|
||||
border-color: var(--accent-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Main Workspace */
|
||||
.main-workspace {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Canvas Viewport */
|
||||
.canvas-viewport {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#dnaCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#dnaCanvas:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Viewport Floating Controls */
|
||||
.viewport-controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btn-ctrl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: var(--text-main);
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-ctrl:hover {
|
||||
background: rgba(56, 189, 248, 0.2);
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.btn-ctrl.active {
|
||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.4) 0%, rgba(56, 189, 248, 0.3) 100%);
|
||||
border-color: var(--accent-cyan);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Organism Hologram Dossier (Right Side) */
|
||||
.dossier-panel {
|
||||
width: 440px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.organism-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.organism-name {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.organism-domain {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.rarity-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rarity-badge.Mythic { background: rgba(236, 72, 153, 0.2); border: 1px solid #ec4899; color: #f472b6; text-shadow: 0 0 10px #ec4899; }
|
||||
.rarity-badge.Legendary { background: rgba(245, 158, 11, 0.2); border: 1px solid #f59e0b; color: #fbbf24; text-shadow: 0 0 10px #f59e0b; }
|
||||
.rarity-badge.Epic { background: rgba(168, 85, 247, 0.2); border: 1px solid #a855f7; color: #c084fc; text-shadow: 0 0 10px #a855f7; }
|
||||
.rarity-badge.Rare { background: rgba(59, 130, 246, 0.2); border: 1px solid #3b82f6; color: #60a5fa; }
|
||||
.rarity-badge.Common { background: rgba(148, 163, 184, 0.2); border: 1px solid #64748b; color: #cbd5e1; }
|
||||
|
||||
.archetype-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* Genetic Metrics Grid */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.metric-num {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.metric-num.purity { color: var(--accent-emerald); }
|
||||
.metric-num.complexity { color: var(--accent-blue); }
|
||||
.metric-num.mutation { color: var(--accent-rose); }
|
||||
|
||||
/* Summary Box */
|
||||
.summary-box {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-left: 3px solid var(--accent-cyan);
|
||||
padding: 10px 14px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
/* Chromosome Palette Bands */
|
||||
.palette-strip {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Base Pair Bars (A-T-C-G) */
|
||||
.base-pair-section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.base-pair-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.base-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.base-bar-wrap {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
margin: 0 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.base-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.base-bar-fill.A { background: #38bdf8; }
|
||||
.base-bar-fill.T { background: #c084fc; }
|
||||
.base-bar-fill.C { background: #fde047; }
|
||||
.base-bar-fill.G { background: #34d399; }
|
||||
|
||||
/* Detail Sub-Cards */
|
||||
.detail-card {
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tracker-tag {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||
color: #fca5a5;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.font-tag {
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
color: #bae6fd;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Comparison Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 800px;
|
||||
max-height: 85vh;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.compare-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>🧬 Website DNA - Genetic Fingerprint of the Web</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/dna.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div style="position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #030712; color: #38bdf8; font-family: sans-serif;">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem; animation: pulse 1.5s infinite;">🧬</div>
|
||||
<h2 style="font-family: 'Orbitron', monospace; font-size: 1.2rem; letter-spacing: 2px;">SEQUENCING DIGITAL GENOMICS...</h2>
|
||||
<p style="color: #64748b; font-size: 0.85rem; margin-top: 0.5rem;">Booting .NET 10 WebAssembly Engine</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" style="display: none; position: fixed; bottom: 0; left: 0; right: 0; background: #7f1d1d; color: white; padding: 12px; text-align: center; z-index: 1000;">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload" style="color: #fca5a5; font-weight: bold; margin-left: 10px;">Reload</a>
|
||||
</div>
|
||||
|
||||
<script src="js/dnaEngine.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,796 @@
|
||||
/**
|
||||
* 🧬 Website DNA Generative Art & Sonification Engine
|
||||
* Morphological Archetype Generative System:
|
||||
* - Minimalist Crystal (Google)
|
||||
* - Textual Coral (Wikipedia / HN)
|
||||
* - Neural HyperWeb (Reddit)
|
||||
* - Design Synthesizer (Stripe / Apple)
|
||||
* - Media Leviathan (YouTube)
|
||||
* - Bloated Chimera (High Trackers / Bloat)
|
||||
*/
|
||||
(function () {
|
||||
window.DnaEngine = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
profile: null,
|
||||
renderMode: 'helix', // 'helix' | 'radial'
|
||||
rotX: 0.25,
|
||||
rotY: 0,
|
||||
rotSpeed: 0.008,
|
||||
zoom: 1.0,
|
||||
isDragging: false,
|
||||
lastMouseX: 0,
|
||||
lastMouseY: 0,
|
||||
time: 0,
|
||||
audioCtx: null,
|
||||
soundEnabled: false,
|
||||
soundTimer: null,
|
||||
soundSequenceIdx: 0,
|
||||
animationFrameId: null,
|
||||
particles: [],
|
||||
arcs: [],
|
||||
dpr: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
|
||||
init: function (canvasId) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
if (!this.canvas) return;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.dpr = window.devicePixelRatio || 1;
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
|
||||
// Mouse / Touch interaction
|
||||
this.canvas.addEventListener('mousedown', (e) => {
|
||||
this.isDragging = true;
|
||||
this.lastMouseX = e.clientX;
|
||||
this.lastMouseY = e.clientY;
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', () => { this.isDragging = false; });
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDragging) return;
|
||||
const dx = e.clientX - this.lastMouseX;
|
||||
const dy = e.clientY - this.lastMouseY;
|
||||
this.rotY += dx * 0.006;
|
||||
this.rotX += dy * 0.006;
|
||||
this.lastMouseX = e.clientX;
|
||||
this.lastMouseY = e.clientY;
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
this.zoom = Math.clamp(this.zoom - e.deltaY * 0.001, 0.4, 3.0);
|
||||
}, { passive: false });
|
||||
|
||||
// Start animation loop
|
||||
this.lastTime = performance.now();
|
||||
this.loop = this.loop.bind(this);
|
||||
this.animationFrameId = requestAnimationFrame(this.loop);
|
||||
|
||||
console.log('🧬 Website DNA Morphological Engine initialized.');
|
||||
},
|
||||
|
||||
resize: function () {
|
||||
if (!this.canvas) return;
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.width = rect.width;
|
||||
this.height = rect.height;
|
||||
this.canvas.width = this.width * this.dpr;
|
||||
this.canvas.height = this.height * this.dpr;
|
||||
this.ctx.scale(this.dpr, this.dpr);
|
||||
},
|
||||
|
||||
setProfile: function (profileJson) {
|
||||
this.profile = typeof profileJson === 'string' ? JSON.parse(profileJson) : profileJson;
|
||||
this.initArchetypeParticles();
|
||||
this.soundSequenceIdx = 0;
|
||||
console.log('🧬 Loaded DNA Profile:', this.profile?.domain, 'Archetype:', this.profile?.archetype);
|
||||
},
|
||||
|
||||
setRenderMode: function (mode) {
|
||||
this.renderMode = mode || 'helix';
|
||||
},
|
||||
|
||||
initArchetypeParticles: function () {
|
||||
this.particles = [];
|
||||
this.arcs = [];
|
||||
if (!this.profile) return;
|
||||
|
||||
const arch = this.profile.archetype || 'NeuralHyperWeb';
|
||||
const colors = this.profile.colorBands?.map(b => b.hex) || ['#38bdf8', '#a855f7', '#10b981', '#f59e0b'];
|
||||
const primaryCol = colors[0] || '#38bdf8';
|
||||
const accentCol = colors[1] || '#a855f7';
|
||||
|
||||
if (arch === 'MinimalistCrystal') {
|
||||
// 4 Sharp orbiting satellite gemstones in geometric formation
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.particles.push({
|
||||
type: 'gem',
|
||||
angle: (Math.PI * 2 / 4) * i,
|
||||
orbitRadius: 180,
|
||||
speed: 0.015,
|
||||
color: colors[i % colors.length],
|
||||
size: 9
|
||||
});
|
||||
}
|
||||
} else if (arch === 'NeuralHyperWeb') {
|
||||
// Swarm of electric sparks and tracker mutagen spores
|
||||
const count = 35 + (this.profile.detectedTrackers?.length || 0) * 10;
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.particles.push({
|
||||
type: 'spark',
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
radius: 60 + Math.random() * 160,
|
||||
y: (Math.random() - 0.5) * 360,
|
||||
speed: 0.02 + Math.random() * 0.03,
|
||||
size: 2 + Math.random() * 5,
|
||||
color: Math.random() < 0.4 ? '#ef4444' : primaryCol,
|
||||
pulse: Math.random() * Math.PI * 2
|
||||
});
|
||||
}
|
||||
} else if (arch === 'TextualCoral') {
|
||||
// Gentle floating ink / parchment spores
|
||||
for (let i = 0; i < 25; i++) {
|
||||
this.particles.push({
|
||||
type: 'fossil',
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
radius: 40 + Math.random() * 120,
|
||||
y: (Math.random() - 0.5) * 400,
|
||||
speed: 0.005 + Math.random() * 0.01,
|
||||
size: 1.5 + Math.random() * 3,
|
||||
color: colors[i % colors.length]
|
||||
});
|
||||
}
|
||||
} else if (arch === 'MediaLeviathan') {
|
||||
// Massive orbiting planetary video/audio organelles with accretion dust
|
||||
for (let i = 0; i < 6; i++) {
|
||||
this.particles.push({
|
||||
type: 'planet',
|
||||
angle: (Math.PI * 2 / 6) * i,
|
||||
radius: 120 + i * 25,
|
||||
y: Math.sin(i) * 60,
|
||||
speed: 0.01 - i * 0.001,
|
||||
size: 8 + (i % 3) * 5,
|
||||
color: colors[i % colors.length]
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 40; i++) {
|
||||
this.particles.push({
|
||||
type: 'dust',
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
radius: 70 + Math.random() * 190,
|
||||
y: (Math.random() - 0.5) * 120,
|
||||
speed: 0.015,
|
||||
size: 1.5,
|
||||
color: accentCol
|
||||
});
|
||||
}
|
||||
} else if (arch === 'DesignSynthesizer') {
|
||||
// Fluid floating aurora light orbs
|
||||
for (let i = 0; i < 18; i++) {
|
||||
this.particles.push({
|
||||
type: 'aurora',
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
radius: 90 + Math.random() * 140,
|
||||
y: (Math.random() - 0.5) * 320,
|
||||
speed: 0.008 + Math.random() * 0.015,
|
||||
size: 4 + Math.random() * 6,
|
||||
color: colors[i % colors.length]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Bloated Chimera: Toxic radioactive mutant spores
|
||||
for (let i = 0; i < 50; i++) {
|
||||
this.particles.push({
|
||||
type: 'toxic',
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
radius: 50 + Math.random() * 180,
|
||||
y: (Math.random() - 0.5) * 420,
|
||||
speed: 0.03 + Math.random() * 0.04,
|
||||
size: 3 + Math.random() * 6,
|
||||
color: i % 2 === 0 ? '#ef4444' : '#10b981'
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initAudio: function () {
|
||||
if (!this.audioCtx) {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContext) this.audioCtx = new AudioContext();
|
||||
}
|
||||
if (this.audioCtx && this.audioCtx.state === 'suspended') {
|
||||
this.audioCtx.resume();
|
||||
}
|
||||
},
|
||||
|
||||
toggleSound: function () {
|
||||
this.soundEnabled = !this.soundEnabled;
|
||||
if (this.soundEnabled) {
|
||||
this.initAudio();
|
||||
this.startSonification();
|
||||
} else {
|
||||
this.stopSonification();
|
||||
}
|
||||
return this.soundEnabled;
|
||||
},
|
||||
|
||||
startSonification: function () {
|
||||
if (this.soundTimer) clearInterval(this.soundTimer);
|
||||
if (!this.profile || !this.profile.geneticSequence) return;
|
||||
|
||||
const seq = this.profile.geneticSequence;
|
||||
const arch = this.profile.archetype || 'NeuralHyperWeb';
|
||||
|
||||
// Scale mapping per archetype
|
||||
const freqScales = {
|
||||
'MinimalistCrystal': { 'A': 523.25, 'T': 659.25, 'C': 783.99, 'G': 1046.50 }, // High pure crystal chimes (C5-C6)
|
||||
'TextualCoral': { 'A': 174.61, 'T': 220.00, 'C': 261.63, 'G': 329.63 }, // Warm wooden marimba (F3-E4)
|
||||
'NeuralHyperWeb': { 'A': 220.00, 'T': 277.18, 'C': 329.63, 'G': 440.00 }, // Synth arpeggios (A3-A4)
|
||||
'DesignSynthesizer': { 'A': 293.66, 'T': 369.99, 'C': 440.00, 'G': 587.33 },// Lush cinematic chords (D4-D5)
|
||||
'MediaLeviathan': { 'A': 130.81, 'T': 164.81, 'C': 196.00, 'G': 246.94 }, // Deep resonant bass (C3-B3)
|
||||
'BloatedChimera': { 'A': 110.00, 'T': 138.59, 'C': 164.81, 'G': 207.65 } // Detuned ominous dark drone
|
||||
};
|
||||
|
||||
const freqs = freqScales[arch] || freqScales['NeuralHyperWeb'];
|
||||
const interval = arch === 'MinimalistCrystal' ? 240 : arch === 'NeuralHyperWeb' ? 140 : 180;
|
||||
|
||||
this.soundTimer = setInterval(() => {
|
||||
if (!this.soundEnabled || !this.audioCtx) return;
|
||||
const base = seq[this.soundSequenceIdx % seq.length];
|
||||
const freq = freqs[base] || 261.63;
|
||||
this.playTone(freq, base, arch);
|
||||
this.soundSequenceIdx++;
|
||||
}, interval);
|
||||
},
|
||||
|
||||
stopSonification: function () {
|
||||
if (this.soundTimer) {
|
||||
clearInterval(this.soundTimer);
|
||||
this.soundTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
playTone: function (freq, base, arch) {
|
||||
if (!this.audioCtx) return;
|
||||
try {
|
||||
const now = this.audioCtx.currentTime;
|
||||
const osc = this.audioCtx.createOscillator();
|
||||
const gain = this.audioCtx.createGain();
|
||||
|
||||
if (arch === 'MinimalistCrystal') osc.type = 'sine';
|
||||
else if (arch === 'NeuralHyperWeb') osc.type = base === 'C' ? 'sawtooth' : 'triangle';
|
||||
else if (arch === 'BloatedChimera') osc.type = 'sawtooth';
|
||||
else if (arch === 'TextualCoral') osc.type = 'triangle';
|
||||
else osc.type = 'sine';
|
||||
|
||||
osc.frequency.setValueAtTime(freq, now);
|
||||
|
||||
const dur = arch === 'MinimalistCrystal' ? 0.6 : 0.35;
|
||||
gain.gain.setValueAtTime(0.04, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.audioCtx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + dur);
|
||||
} catch (e) { }
|
||||
},
|
||||
|
||||
exportCardPng: function () {
|
||||
if (!this.canvas) return;
|
||||
const dataUrl = this.canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.download = `${this.profile?.domain || 'website'}_dna_card.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
},
|
||||
|
||||
loop: function (currentTime) {
|
||||
const dt = Math.min(0.1, (currentTime - this.lastTime) / 1000);
|
||||
this.lastTime = currentTime;
|
||||
this.time += dt;
|
||||
|
||||
if (!this.isDragging) {
|
||||
this.rotY += this.rotSpeed;
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.animationFrameId = requestAnimationFrame(this.loop);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const ctx = this.ctx;
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
if (!ctx || w === 0 || h === 0) return;
|
||||
|
||||
// 1. Dynamic Atmosphere Background customized to Website's Palette
|
||||
this.renderCustomAtmosphere(ctx, w, h);
|
||||
|
||||
if (!this.profile) {
|
||||
this.renderPlaceholder(ctx, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(w / 2, h / 2);
|
||||
|
||||
if (this.renderMode === 'helix') {
|
||||
this.renderMorphologicalHelix(ctx, w, h);
|
||||
} else {
|
||||
this.renderRadialMandala(ctx, w, h);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
renderCustomAtmosphere: function (ctx, w, h) {
|
||||
const colors = this.profile?.colorBands?.map(b => b.hex) || ['#06b6d4', '#3b82f6'];
|
||||
const pCol = colors[0] || '#030712';
|
||||
const sCol = colors[1] || '#000000';
|
||||
|
||||
// Deep background gradient reflecting website's primary theme
|
||||
const bgGrad = ctx.createLinearGradient(0, 0, w, h);
|
||||
bgGrad.addColorStop(0, '#030712');
|
||||
bgGrad.addColorStop(0.5, '#050b14');
|
||||
bgGrad.addColorStop(1, '#020408');
|
||||
ctx.fillStyle = bgGrad;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Large chromatic bloom
|
||||
const bloom = ctx.createRadialGradient(w / 2, h / 2, 40, w / 2, h / 2, Math.max(w, h) * 0.55);
|
||||
bloom.addColorStop(0, hexToRgba(pCol, 0.16));
|
||||
bloom.addColorStop(0.4, hexToRgba(sCol, 0.06));
|
||||
bloom.addColorStop(1, 'rgba(3, 7, 18, 0)');
|
||||
ctx.fillStyle = bloom;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Sci-fi coordinate grid lines
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < w; x += 60) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, h);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < h; y += 60) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
renderPlaceholder: function (ctx, w, h) {
|
||||
ctx.save();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#64748b';
|
||||
ctx.font = '14px "JetBrains Mono", monospace';
|
||||
ctx.fillText('ENTER A WEBSITE URL TO SEQUENCE ITS GENOME', w / 2, h / 2);
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
// --- MORPHOLOGICAL 3D HELIX RENDERER ---
|
||||
renderMorphologicalHelix: function (ctx, w, h) {
|
||||
const arch = this.profile.archetype || 'NeuralHyperWeb';
|
||||
const seq = this.profile.geneticSequence || 'ATCG';
|
||||
const colors = this.profile.colorBands?.map(b => b.hex) || ['#38bdf8', '#a855f7', '#10b981', '#f59e0b'];
|
||||
const dom = this.profile.domMetrics || { maxDepth: 12, totalNodes: 500 };
|
||||
|
||||
// Dynamic morphological scaling based on website metrics
|
||||
let numRungs = Math.min(seq.length, 65);
|
||||
let radius = (70 + dom.maxDepth * 2.5) * this.zoom;
|
||||
let rungSpacing = (7 + (dom.totalNodes > 1000 ? 1.5 : 0)) * this.zoom;
|
||||
let totalHeight = numRungs * rungSpacing;
|
||||
let startY = -totalHeight / 2;
|
||||
|
||||
// Archetype specific tuning
|
||||
if (arch === 'MinimalistCrystal') {
|
||||
radius = 65 * this.zoom;
|
||||
numRungs = 40;
|
||||
totalHeight = numRungs * (rungSpacing * 1.3);
|
||||
} else if (arch === 'NeuralHyperWeb') {
|
||||
radius = 110 * this.zoom; // Wide chaotic web
|
||||
} else if (arch === 'TextualCoral') {
|
||||
radius = 80 * this.zoom;
|
||||
rungSpacing = 5.5 * this.zoom; // Very dense horizontal strata
|
||||
} else if (arch === 'DesignSynthesizer') {
|
||||
radius = 95 * this.zoom;
|
||||
}
|
||||
|
||||
const drawList = [];
|
||||
|
||||
// 1. Build Rungs & Strands
|
||||
for (let i = 0; i < numRungs; i++) {
|
||||
const base = seq[i % seq.length];
|
||||
let angle = i * 0.35 + this.rotY;
|
||||
|
||||
// Add organic noise / chaos for high JS or tracker mutation
|
||||
if (arch === 'NeuralHyperWeb') {
|
||||
angle += Math.sin(this.time * 2 + i * 0.2) * 0.08;
|
||||
} else if (arch === 'BloatedChimera') {
|
||||
angle += Math.sin(this.time * 5 + i * 0.5) * 0.18; // Glitch wobble
|
||||
}
|
||||
|
||||
const curY = startY + i * rungSpacing;
|
||||
const rVar = arch === 'NeuralHyperWeb' ? radius * (1 + Math.sin(i * 0.4) * 0.2) : radius;
|
||||
|
||||
// Strand 1 in 3D
|
||||
const x1 = Math.cos(angle) * rVar;
|
||||
const z1 = Math.sin(angle) * rVar;
|
||||
const p1 = this.project3D(x1, curY, z1);
|
||||
|
||||
// Strand 2 in 3D (Opposite)
|
||||
const x2 = Math.cos(angle + Math.PI) * rVar;
|
||||
const z2 = Math.sin(angle + Math.PI) * rVar;
|
||||
const p2 = this.project3D(x2, curY, z2);
|
||||
|
||||
const avgZ = (z1 + z2) / 2;
|
||||
const rungCol = colors[i % colors.length] || '#38bdf8';
|
||||
|
||||
drawList.push({
|
||||
type: 'rung',
|
||||
base: base,
|
||||
color: rungCol,
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
z: avgZ,
|
||||
index: i
|
||||
});
|
||||
|
||||
// In NeuralHyperWeb, occasionally add cross-synaptic lightning arcs!
|
||||
if (arch === 'NeuralHyperWeb' && i % 4 === 0 && i + 3 < numRungs) {
|
||||
drawList.push({
|
||||
type: 'synapse',
|
||||
p1: p1,
|
||||
targetIndex: i + 3,
|
||||
z: avgZ
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Add Orbiting Particles / Organelles
|
||||
this.particles.forEach((pt, pIdx) => {
|
||||
const a = pt.angle + this.rotY * (pt.speed ? pt.speed * 80 : 1);
|
||||
const pRadius = (pt.radius || pt.orbitRadius || 120) * this.zoom;
|
||||
const px = Math.cos(a) * pRadius;
|
||||
const pz = Math.sin(a) * pRadius;
|
||||
const py = (pt.y || 0) * this.zoom;
|
||||
|
||||
const proj = this.project3D(px, py, pz);
|
||||
drawList.push({
|
||||
type: 'particle',
|
||||
pType: pt.type,
|
||||
proj: proj,
|
||||
color: pt.color,
|
||||
size: (pt.size || 4) * this.zoom,
|
||||
z: pz
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by 3D Z-Depth (Painter's Algorithm)
|
||||
drawList.sort((a, b) => a.z - b.z);
|
||||
|
||||
// 3. Render Sorted Elements
|
||||
drawList.forEach(item => {
|
||||
if (item.type === 'rung') {
|
||||
this.drawArchetypeRung(ctx, item, arch);
|
||||
} else if (item.type === 'particle') {
|
||||
this.drawArchetypeParticle(ctx, item, arch);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Central Core Energy Beam / Laser
|
||||
this.renderArchetypeCenterCore(ctx, startY, totalHeight, arch, colors);
|
||||
},
|
||||
|
||||
drawArchetypeRung: function (ctx, item, arch) {
|
||||
const p1 = item.p1;
|
||||
const p2 = item.p2;
|
||||
const col = item.color;
|
||||
const sc = p1.scale;
|
||||
|
||||
if (arch === 'MinimalistCrystal') {
|
||||
// Precision laser beam with diamond facets
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = col;
|
||||
ctx.lineWidth = 2 * sc;
|
||||
ctx.shadowColor = col;
|
||||
ctx.shadowBlur = 10 * sc;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Diamond facet node
|
||||
drawDiamond(ctx, p1.x, p1.y, 6 * sc, col);
|
||||
drawDiamond(ctx, p2.x, p2.y, 6 * sc, '#ffffff');
|
||||
|
||||
} else if (arch === 'TextualCoral') {
|
||||
// Textured organic parchment rungs
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = col;
|
||||
ctx.lineWidth = 3 * sc;
|
||||
ctx.stroke();
|
||||
|
||||
// Round node
|
||||
ctx.beginPath();
|
||||
ctx.arc(p1.x, p1.y, 4 * sc, 0, Math.PI * 2);
|
||||
ctx.fillStyle = col;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(p2.x, p2.y, 4 * sc, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#f8fafc';
|
||||
ctx.fill();
|
||||
|
||||
} else if (arch === 'NeuralHyperWeb') {
|
||||
// Glowing electric synaptic bridge
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = col;
|
||||
ctx.lineWidth = 2.5 * sc;
|
||||
ctx.shadowColor = col;
|
||||
ctx.shadowBlur = 12 * sc;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Pulsing synapse node
|
||||
ctx.beginPath();
|
||||
ctx.arc(p1.x, p1.y, (4 + Math.sin(this.time * 4 + item.index) * 2) * sc, 0, Math.PI * 2);
|
||||
ctx.fillStyle = col;
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p2.x, p2.y, 4 * sc, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#ff4500';
|
||||
ctx.fill();
|
||||
|
||||
} else if (arch === 'DesignSynthesizer') {
|
||||
// Fluid metallic gradient ribbon
|
||||
const ribGrad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
|
||||
ribGrad.addColorStop(0, col);
|
||||
ribGrad.addColorStop(1, '#00d4ff');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = ribGrad;
|
||||
ctx.lineWidth = 3.5 * sc;
|
||||
ctx.shadowColor = col;
|
||||
ctx.shadowBlur = 14 * sc;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Glass bead node
|
||||
ctx.beginPath();
|
||||
ctx.arc(p1.x, p1.y, 5 * sc, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fill();
|
||||
|
||||
} else {
|
||||
// Bloated Chimera: Jagged toxic bone rung
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = col;
|
||||
ctx.lineWidth = 3 * sc;
|
||||
ctx.shadowColor = '#ef4444';
|
||||
ctx.shadowBlur = 12 * sc;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p1.x, p1.y, 5 * sc, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.fill();
|
||||
}
|
||||
},
|
||||
|
||||
drawArchetypeParticle: function (ctx, item, arch) {
|
||||
const sc = item.proj.scale;
|
||||
const x = item.proj.x;
|
||||
const y = item.proj.y;
|
||||
const sz = item.size * sc;
|
||||
|
||||
if (item.pType === 'gem') {
|
||||
// Minimalist Gemstone
|
||||
drawDiamond(ctx, x, y, sz * 1.4, item.color);
|
||||
} else if (item.pType === 'planet') {
|
||||
// Media Leviathan Planet Organelle with Rings
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, sz, 0, Math.PI * 2);
|
||||
ctx.fillStyle = item.color;
|
||||
ctx.shadowColor = item.color;
|
||||
ctx.shadowBlur = 16 * sc;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Planetary ring
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, sz * 2.2, sz * 0.7, Math.PI / 6, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
// Glowing particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, sz, 0, Math.PI * 2);
|
||||
ctx.fillStyle = item.color;
|
||||
ctx.shadowColor = item.color;
|
||||
ctx.shadowBlur = 10 * sc;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
},
|
||||
|
||||
renderArchetypeCenterCore: function (ctx, startY, totalHeight, arch, colors) {
|
||||
ctx.save();
|
||||
const topY = startY * this.zoom;
|
||||
const botY = (startY + totalHeight) * this.zoom;
|
||||
|
||||
if (arch === 'MinimalistCrystal') {
|
||||
// Ultra-thin surgical neon laser core
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, topY);
|
||||
ctx.lineTo(0, botY);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.shadowColor = colors[0] || '#4285f4';
|
||||
ctx.shadowBlur = 18;
|
||||
ctx.stroke();
|
||||
|
||||
} else if (arch === 'MediaLeviathan') {
|
||||
// Massive pulsing gravitational singularity core
|
||||
const coreGrad = ctx.createRadialGradient(0, 0, 10, 0, 0, 60 * this.zoom);
|
||||
coreGrad.addColorStop(0, '#ffffff');
|
||||
coreGrad.addColorStop(0.4, colors[0] || '#ff0000');
|
||||
coreGrad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = coreGrad;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 60 * this.zoom, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
} else {
|
||||
// Chromosome Spectral Ribbon
|
||||
const beamGrad = ctx.createLinearGradient(0, topY, 0, botY);
|
||||
colors.forEach((c, idx) => {
|
||||
const stop = idx / Math.max(1, colors.length - 1);
|
||||
beamGrad.addColorStop(stop, hexToRgba(c, 0.5));
|
||||
});
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-5, topY);
|
||||
ctx.lineTo(5, topY);
|
||||
ctx.lineTo(5, botY);
|
||||
ctx.lineTo(-5, botY);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = beamGrad;
|
||||
ctx.shadowColor = colors[0] || '#38bdf8';
|
||||
ctx.shadowBlur = 16;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
project3D: function (x, y, z) {
|
||||
const cosX = Math.cos(this.rotX);
|
||||
const sinX = Math.sin(this.rotX);
|
||||
const y1 = y * cosX - z * sinX;
|
||||
const z1 = y * sinX + z * cosX;
|
||||
|
||||
const fov = 450;
|
||||
const scale = fov / (fov + z1);
|
||||
return {
|
||||
x: x * scale,
|
||||
y: y1 * scale,
|
||||
scale: scale
|
||||
};
|
||||
},
|
||||
|
||||
// --- 2. RADIAL GENOMIC MANDALA RENDERER ---
|
||||
renderRadialMandala: function (ctx, w, h) {
|
||||
const seq = this.profile.geneticSequence || 'ATCG';
|
||||
const colors = this.profile.colorBands?.map(b => b.hex) || ['#38bdf8', '#a855f7', '#10b981', '#f59e0b'];
|
||||
const maxR = Math.min(w, h) * 0.38 * this.zoom;
|
||||
const rings = 6;
|
||||
|
||||
// 1. Concentric Depth Rings
|
||||
for (let r = 1; r <= rings; r++) {
|
||||
const radius = (maxR / rings) * r;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, radius, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${0.05 + r * 0.02})`;
|
||||
ctx.setLineDash([4, 8]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// 2. Chromosome Petal Sectors
|
||||
const angleStep = (Math.PI * 2) / Math.max(1, colors.length);
|
||||
colors.forEach((c, idx) => {
|
||||
const a = idx * angleStep + this.rotY;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.arc(0, 0, maxR * 0.5, a, a + angleStep);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = hexToRgba(c, 0.35);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// 3. Genetic Base Rays
|
||||
const numRays = Math.min(seq.length, 80);
|
||||
const rayStep = (Math.PI * 2) / numRays;
|
||||
|
||||
for (let i = 0; i < numRays; i++) {
|
||||
const col = colors[i % colors.length] || '#38bdf8';
|
||||
const a = i * rayStep + this.rotY;
|
||||
const r1 = maxR * 0.55;
|
||||
const r2 = maxR * (0.8 + (i % 4) * 0.08);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.cos(a) * r1, Math.sin(a) * r1);
|
||||
ctx.lineTo(Math.cos(a) * r2, Math.sin(a) * r2);
|
||||
ctx.strokeStyle = col;
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Spore node
|
||||
ctx.beginPath();
|
||||
ctx.arc(Math.cos(a) * r2, Math.sin(a) * r2, 3.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = col;
|
||||
ctx.shadowColor = col;
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// 4. Center Bio-Orb
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 26 * this.zoom, 0, Math.PI * 2);
|
||||
ctx.fillStyle = colors[0] || '#38bdf8';
|
||||
ctx.shadowColor = colors[0] || '#38bdf8';
|
||||
ctx.shadowBlur = 22;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
};
|
||||
|
||||
function drawDiamond(ctx, x, y, size, color) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y - size);
|
||||
ctx.lineTo(x + size, y);
|
||||
ctx.lineTo(x, y + size);
|
||||
ctx.lineTo(x - size, y);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = color;
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
if (!hex) return `rgba(56, 189, 248, ${alpha})`;
|
||||
let c = hex.replace('#', '');
|
||||
if (c.length === 3) c = c.split('').map(x => x + x).join('');
|
||||
const num = parseInt(c, 16);
|
||||
return `rgba(${(num >> 16) & 255}, ${(num >> 8) & 255}, ${num & 255}, ${alpha})`;
|
||||
}
|
||||
|
||||
Math.clamp = function (val, min, max) {
|
||||
return Math.max(min, Math.min(max, val));
|
||||
};
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+4084
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+594
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5402
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5393
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"date": "2022-01-06",
|
||||
"temperatureC": 1,
|
||||
"summary": "Freezing"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-07",
|
||||
"temperatureC": 14,
|
||||
"summary": "Bracing"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-08",
|
||||
"temperatureC": -13,
|
||||
"summary": "Freezing"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-09",
|
||||
"temperatureC": -16,
|
||||
"summary": "Balmy"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-10",
|
||||
"temperatureC": -2,
|
||||
"summary": "Chilly"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WebsiteDna.Server.Services;
|
||||
using WebsiteDna.Shared;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", policy =>
|
||||
{
|
||||
policy.SetIsOriginAllowed(_ => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient<IWebsiteAnalyzerService, WebsiteAnalyzerService>(client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
// API Endpoints
|
||||
app.MapPost("/api/dna/analyze", async ([FromBody] AnalysisRequest req, IWebsiteAnalyzerService analyzer, CancellationToken ct) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.Url))
|
||||
{
|
||||
return Results.BadRequest(new { error = "URL is required" });
|
||||
}
|
||||
|
||||
var profile = await analyzer.AnalyzeUrlAsync(req.Url, ct);
|
||||
return Results.Ok(profile);
|
||||
});
|
||||
|
||||
app.MapGet("/api/dna/presets", (IWebsiteAnalyzerService analyzer) =>
|
||||
{
|
||||
return Results.Ok(analyzer.GetPresets());
|
||||
});
|
||||
|
||||
app.MapPost("/api/dna/compare", ([FromBody] CompareRequest req, IWebsiteAnalyzerService analyzer, CancellationToken ct) =>
|
||||
{
|
||||
var presets = analyzer.GetPresets();
|
||||
var pA = presets.FirstOrDefault(p => p.Domain.Equals(req.Url1, StringComparison.OrdinalIgnoreCase) || p.Url.Equals(req.Url1, StringComparison.OrdinalIgnoreCase))
|
||||
?? analyzer.AnalyzeUrlAsync(req.Url1, ct).GetAwaiter().GetResult();
|
||||
var pB = presets.FirstOrDefault(p => p.Domain.Equals(req.Url2, StringComparison.OrdinalIgnoreCase) || p.Url.Equals(req.Url2, StringComparison.OrdinalIgnoreCase))
|
||||
?? analyzer.AnalyzeUrlAsync(req.Url2, ct).GetAwaiter().GetResult();
|
||||
|
||||
var comparison = analyzer.CompareProfiles(pA, pB);
|
||||
return Results.Ok(comparison);
|
||||
});
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new { status = "Healthy", service = "Website DNA Genomics API", version = "1.0.0-net10" }));
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5060",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7060;http://localhost:5060",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.RegularExpressions;
|
||||
using WebsiteDna.Shared;
|
||||
|
||||
namespace WebsiteDna.Server.Services;
|
||||
|
||||
public interface IWebsiteAnalyzerService
|
||||
{
|
||||
Task<WebsiteDnaProfile> AnalyzeUrlAsync(string rawUrl, CancellationToken ct = default);
|
||||
ComparisonResult CompareProfiles(WebsiteDnaProfile profileA, WebsiteDnaProfile profileB);
|
||||
List<WebsiteDnaProfile> GetPresets();
|
||||
}
|
||||
|
||||
public class WebsiteAnalyzerService : IWebsiteAnalyzerService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<WebsiteAnalyzerService> _logger;
|
||||
|
||||
private static readonly (string Name, string Pattern)[] KnownTrackers =
|
||||
[
|
||||
("Google Tag Manager", @"googletagmanager\.com/gtm\.js"),
|
||||
("Google Analytics 4", @"google-analytics\.com|googletagmanager\.com/gtag"),
|
||||
("Meta Pixel", @"connect\.facebook\.net/.*fbevents\.js"),
|
||||
("Hotjar", @"static\.hotjar\.com"),
|
||||
("Microsoft Clarity", @"clarity\.ms/tag"),
|
||||
("Sentry Error Tracking", @"browser\.sentry-cdn\.com|sentry\.io"),
|
||||
("Segment.io", @"cdn\.segment\.com/analytics\.js"),
|
||||
("Mixpanel", @"cdn\.mxpnl\.com"),
|
||||
("TikTok Pixel", @"analytics\.tiktok\.com"),
|
||||
("Intercom Chat", @"widget\.intercom\.io")
|
||||
];
|
||||
|
||||
public WebsiteAnalyzerService(HttpClient httpClient, ILogger<WebsiteAnalyzerService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<WebsiteDnaProfile> GetPresets() => WebsiteDnaPresets.GetPresets();
|
||||
|
||||
public async Task<WebsiteDnaProfile> AnalyzeUrlAsync(string rawUrl, CancellationToken ct = default)
|
||||
{
|
||||
var normalizedUrl = NormalizeUrl(rawUrl);
|
||||
var uri = new Uri(normalizedUrl);
|
||||
var domain = uri.Host.ToLowerInvariant().Replace("www.", "");
|
||||
|
||||
// Check if matching preset exists for instant lookup
|
||||
var existingPreset = GetPresets().FirstOrDefault(p => p.Domain.Equals(domain, StringComparison.OrdinalIgnoreCase));
|
||||
if (existingPreset != null && rawUrl.Equals(existingPreset.Domain, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return existingPreset;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, normalizedUrl);
|
||||
request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36");
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var html = await response.Content.ReadAsStringAsync(ct);
|
||||
return ParseHtmlAndBuildProfile(normalizedUrl, domain, html);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Live analysis failed for {Url}. Generating deterministic synthetic DNA fallback.", normalizedUrl);
|
||||
return GenerateSyntheticProfile(normalizedUrl, domain);
|
||||
}
|
||||
}
|
||||
|
||||
private WebsiteDnaProfile ParseHtmlAndBuildProfile(string url, string domain, string html)
|
||||
{
|
||||
var profile = new WebsiteDnaProfile
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Url = url,
|
||||
Domain = domain,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Extract Title
|
||||
var titleMatch = Regex.Match(html, @"<title[^>]*>(.*?)</title>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
profile.Title = titleMatch.Success ? titleMatch.Groups[1].Value.Trim() : domain;
|
||||
|
||||
// Extract Favicon
|
||||
var iconMatch = Regex.Match(html, @"<link[^>]*rel=[""'](?:shortcut )?icon[""'][^>]*href=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||
if (iconMatch.Success)
|
||||
{
|
||||
var href = iconMatch.Groups[1].Value;
|
||||
profile.FaviconUrl = href.StartsWith("http") ? href : new Uri(new Uri(url), href).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
profile.FaviconUrl = $"https://{domain}/favicon.ico";
|
||||
}
|
||||
|
||||
// DOM Metrics & Tags extraction
|
||||
var tags = Regex.Matches(html, @"<\s*([a-zA-Z0-9\-]+)(?:\s+[^>]*)?>", RegexOptions.IgnoreCase);
|
||||
var totalTags = tags.Count;
|
||||
|
||||
var dom = new DomMetricsDto { TotalNodes = totalTags };
|
||||
var semanticTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"header", "nav", "main", "article", "section", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "p", "figure", "figcaption", "time"
|
||||
};
|
||||
|
||||
int depth = 0;
|
||||
int maxDepth = 1;
|
||||
|
||||
foreach (Match tag in tags)
|
||||
{
|
||||
var tagName = tag.Groups[1].Value.ToLowerInvariant();
|
||||
if (semanticTags.Contains(tagName)) dom.SemanticElements++;
|
||||
|
||||
switch (tagName)
|
||||
{
|
||||
case "div": dom.DivCount++; depth++; break;
|
||||
case "span": dom.SpanCount++; break;
|
||||
case "script": dom.ScriptCount++; break;
|
||||
case "style": case "link": dom.StyleCount++; break;
|
||||
case "img": dom.ImgCount++; break;
|
||||
case "svg": dom.SvgCount++; break;
|
||||
case "button": dom.ButtonCount++; break;
|
||||
case "form": dom.FormCount++; break;
|
||||
}
|
||||
|
||||
if (depth > maxDepth) maxDepth = depth;
|
||||
if (html.Contains($"</{tagName}>")) depth = Math.Max(0, depth - 1);
|
||||
}
|
||||
|
||||
dom.MaxDepth = Math.Clamp(maxDepth, 4, 38);
|
||||
dom.SemanticRatio = totalTags > 0 ? Math.Round((double)dom.SemanticElements / totalTags, 2) : 0.5;
|
||||
profile.DomMetrics = dom;
|
||||
|
||||
// Assets Footprint estimation
|
||||
var htmlBytes = html.Length * sizeof(char) / 2;
|
||||
profile.AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
HtmlBytes = htmlBytes,
|
||||
CssBytes = dom.StyleCount * 28_000L,
|
||||
JsBytes = dom.ScriptCount * 45_000L,
|
||||
ImageBytes = (dom.ImgCount + dom.SvgCount) * 35_000L,
|
||||
FontBytes = 65_000L
|
||||
};
|
||||
profile.AssetFootprint.TotalBytes = profile.AssetFootprint.HtmlBytes + profile.AssetFootprint.CssBytes +
|
||||
profile.AssetFootprint.JsBytes + profile.AssetFootprint.ImageBytes + profile.AssetFootprint.FontBytes;
|
||||
|
||||
// Color extraction (Regex for hex colors)
|
||||
var hexMatches = Regex.Matches(html, @"#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b");
|
||||
var colorCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in hexMatches)
|
||||
{
|
||||
var hex = m.Value.ToLowerInvariant();
|
||||
if (hex.Length == 4) hex = $"#{hex[1]}{hex[1]}{hex[2]}{hex[2]}{hex[3]}{hex[3]}";
|
||||
if (hex is not "#ffffff" and not "#000000" and not "#0f172a")
|
||||
{
|
||||
colorCounts[hex] = colorCounts.GetValueOrDefault(hex) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
var topColors = colorCounts.OrderByDescending(kv => kv.Value).Take(4).ToList();
|
||||
if (topColors.Count == 0)
|
||||
{
|
||||
topColors.Add(new KeyValuePair<string, int>("#06b6d4", 40));
|
||||
topColors.Add(new KeyValuePair<string, int>("#3b82f6", 30));
|
||||
topColors.Add(new KeyValuePair<string, int>("#10b981", 20));
|
||||
topColors.Add(new KeyValuePair<string, int>("#f59e0b", 10));
|
||||
}
|
||||
|
||||
var sumC = (double)topColors.Sum(c => c.Value);
|
||||
profile.ColorBands = topColors.Select(c => new ColorBandDto
|
||||
{
|
||||
Hex = c.Key,
|
||||
Percentage = Math.Round((c.Value / sumC) * 100, 1),
|
||||
Role = "Dominant Chromosome"
|
||||
}).ToList();
|
||||
|
||||
// Font detection
|
||||
var fontMatches = Regex.Matches(html, @"font-family\s*:\s*([^;\}]+)", RegexOptions.IgnoreCase);
|
||||
var fonts = new HashSet<string>();
|
||||
foreach (Match m in fontMatches)
|
||||
{
|
||||
var family = m.Groups[1].Value.Split(',')[0].Trim('\'', '"', ' ');
|
||||
if (!string.IsNullOrWhiteSpace(family) && family.Length < 30)
|
||||
{
|
||||
fonts.Add(family);
|
||||
}
|
||||
}
|
||||
profile.DetectedFonts = fonts.Count > 0 ? fonts.Take(4).ToList() : ["Inter", "system-ui", "sans-serif"];
|
||||
|
||||
// Trackers & Mutations detection
|
||||
var trackers = new List<string>();
|
||||
foreach (var (name, pattern) in KnownTrackers)
|
||||
{
|
||||
if (Regex.IsMatch(html, pattern, RegexOptions.IgnoreCase))
|
||||
{
|
||||
trackers.Add(name);
|
||||
}
|
||||
}
|
||||
profile.DetectedTrackers = trackers;
|
||||
|
||||
// Base percentages (A, T, C, G)
|
||||
int pA = Math.Max(15, (int)(dom.SemanticRatio * 60));
|
||||
int pT = Math.Max(15, dom.StyleCount * 4 + 15);
|
||||
int pC = Math.Max(10, dom.ScriptCount * 3 + 10);
|
||||
int pG = Math.Max(10, (dom.ImgCount + dom.SvgCount) * 2 + 10);
|
||||
int totalP = pA + pT + pC + pG;
|
||||
|
||||
profile.GeneticSequence = GenerateSequence(pA, pT, pC, pG, 130);
|
||||
profile.BaseCounts = new Dictionary<string, int>
|
||||
{
|
||||
{ "A", (int)(pA * 130.0 / totalP) },
|
||||
{ "T", (int)(pT * 130.0 / totalP) },
|
||||
{ "C", (int)(pC * 130.0 / totalP) },
|
||||
{ "G", (int)(pG * 130.0 / totalP) }
|
||||
};
|
||||
|
||||
// Scores and Archetype
|
||||
profile.MutationScore = Math.Min(100, trackers.Count * 18.0 + (dom.ScriptCount > 25 ? 20 : 0));
|
||||
profile.ComplexityScore = Math.Min(100, (dom.TotalNodes / 30.0) + dom.MaxDepth * 1.5);
|
||||
profile.PurityScore = Math.Clamp(100.0 - (profile.MutationScore * 0.4) - (dom.DivCount > 500 ? 15 : 0) + (dom.SemanticRatio * 20), 10, 99);
|
||||
|
||||
ClassifyOrganism(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void ClassifyOrganism(WebsiteDnaProfile profile)
|
||||
{
|
||||
if (profile.MutationScore > 50 || profile.AssetFootprint.TotalBytes > 6_000_000)
|
||||
{
|
||||
profile.Archetype = OrganismArchetype.BloatedChimera;
|
||||
profile.Rarity = GeneticRarity.Common;
|
||||
profile.GeneticSummary = "Heavy mutant bio-structure with significant script overhead and tracking mutations.";
|
||||
}
|
||||
else if (profile.DomMetrics.TotalNodes < 300 && profile.PurityScore > 85)
|
||||
{
|
||||
profile.Archetype = OrganismArchetype.MinimalistCrystal;
|
||||
profile.Rarity = profile.PurityScore > 95 ? GeneticRarity.Mythic : GeneticRarity.Legendary;
|
||||
profile.GeneticSummary = "Surgical crystalline organism with high nucleotide purity and minimal DOM divergence.";
|
||||
}
|
||||
else if (profile.DomMetrics.SemanticRatio > 0.65)
|
||||
{
|
||||
profile.Archetype = OrganismArchetype.TextualCoral;
|
||||
profile.Rarity = GeneticRarity.Epic;
|
||||
profile.GeneticSummary = "Ancient textual coral structure. Dense content lattice supported by robust semantic tags.";
|
||||
}
|
||||
else if (profile.DomMetrics.ImgCount + profile.DomMetrics.SvgCount > 40)
|
||||
{
|
||||
profile.Archetype = OrganismArchetype.MediaLeviathan;
|
||||
profile.Rarity = GeneticRarity.Epic;
|
||||
profile.GeneticSummary = "High-energy media leviathan with massive asset organelles and visual resonance.";
|
||||
}
|
||||
else if (profile.PurityScore > 80 && profile.DomMetrics.StyleCount > 5)
|
||||
{
|
||||
profile.Archetype = OrganismArchetype.DesignSynthesizer;
|
||||
profile.Rarity = GeneticRarity.Legendary;
|
||||
profile.GeneticSummary = "Polished design synthesizer with complex CSS chromatic harmonics.";
|
||||
}
|
||||
else
|
||||
{
|
||||
profile.Archetype = OrganismArchetype.NeuralHyperWeb;
|
||||
profile.Rarity = GeneticRarity.Rare;
|
||||
profile.GeneticSummary = "Intricate interactive neural web with dense dynamic branching and active event listeners.";
|
||||
}
|
||||
}
|
||||
|
||||
public ComparisonResult CompareProfiles(WebsiteDnaProfile profileA, WebsiteDnaProfile profileB)
|
||||
{
|
||||
// Compute Genetic Similarity
|
||||
int matchCount = 0;
|
||||
int len = Math.Min(profileA.GeneticSequence.Length, profileB.GeneticSequence.Length);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (profileA.GeneticSequence[i] == profileB.GeneticSequence[i]) matchCount++;
|
||||
}
|
||||
double seqSimilarity = len > 0 ? (double)matchCount / len : 0.5;
|
||||
|
||||
// Archetype alignment
|
||||
double archetypeScore = profileA.Archetype == profileB.Archetype ? 1.0 : 0.4;
|
||||
|
||||
// Purity alignment
|
||||
double purityDelta = Math.Abs(profileA.PurityScore - profileB.PurityScore) / 100.0;
|
||||
double purityScore = 1.0 - purityDelta;
|
||||
|
||||
double overallSimilarity = Math.Round((seqSimilarity * 0.4 + archetypeScore * 0.3 + purityScore * 0.3) * 100, 1);
|
||||
|
||||
string verdict = overallSimilarity switch
|
||||
{
|
||||
> 85 => $"🧬 Clonal Affinity ({overallSimilarity}%): Both sites share virtually identical genetic architecture and chromatic balance.",
|
||||
> 65 => $"🌿 Symbiotic Relatives ({overallSimilarity}%): Similar structural depth and design genus with minor mutation variances.",
|
||||
> 45 => $"⚡ Divergent Species ({overallSimilarity}%): Different structural archetypes with contrasting asset distributions.",
|
||||
_ => $"🪐 Alien Incompatibility ({overallSimilarity}%): Fundamentally opposite genomes (e.g. Minimalist Crystal vs Bloated Chimera)."
|
||||
};
|
||||
|
||||
return new ComparisonResult
|
||||
{
|
||||
ProfileA = profileA,
|
||||
ProfileB = profileB,
|
||||
GeneticSimilarityPercent = overallSimilarity,
|
||||
CompatibilityVerdict = verdict
|
||||
};
|
||||
}
|
||||
|
||||
private WebsiteDnaProfile GenerateSyntheticProfile(string url, string domain)
|
||||
{
|
||||
var seed = Math.Abs(domain.GetHashCode());
|
||||
var rnd = new Random(seed);
|
||||
|
||||
var colors = new[]
|
||||
{
|
||||
new[] { "#38bdf8", "#0284c7", "#06b6d4", "#0f172a" },
|
||||
new[] { "#10b981", "#059669", "#34d399", "#064e3b" },
|
||||
new[] { "#8b5cf6", "#6d28d9", "#c084fc", "#1e1b4b" },
|
||||
new[] { "#f43f5e", "#be123c", "#fb7185", "#4c0519" }
|
||||
}[seed % 4];
|
||||
|
||||
var p = new WebsiteDnaProfile
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Url = url,
|
||||
Domain = domain,
|
||||
Title = $"{char.ToUpper(domain[0])}{domain[1..]} Digital Organism",
|
||||
FaviconUrl = $"https://{domain}/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = (OrganismArchetype)(seed % 5),
|
||||
Rarity = (GeneticRarity)(seed % 6),
|
||||
PurityScore = Math.Round(65.0 + (rnd.NextDouble() * 32.0), 1),
|
||||
ComplexityScore = Math.Round(30.0 + (rnd.NextDouble() * 60.0), 1),
|
||||
MutationScore = Math.Round(5.0 + (rnd.NextDouble() * 30.0), 1),
|
||||
GeneticSummary = "Synthesized genetic organism based on algorithmic domain spectral hashing.",
|
||||
GeneticSequence = GenerateSequence(30 + rnd.Next(20), 25 + rnd.Next(20), 20 + rnd.Next(20), 15 + rnd.Next(15), 130),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = colors[0], Percentage = 40, Role = "Primary Helix" },
|
||||
new ColorBandDto { Hex = colors[1], Percentage = 30, Role = "Secondary Ribbon" },
|
||||
new ColorBandDto { Hex = colors[2], Percentage = 20, Role = "Accent Luminescence" },
|
||||
new ColorBandDto { Hex = colors[3], Percentage = 10, Role = "Core Base" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 42 }, { "T", 36 }, { "C", 28 }, { "G", 24 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = rnd.Next(400, 2400),
|
||||
MaxDepth = rnd.Next(10, 26),
|
||||
SemanticElements = rnd.Next(40, 200),
|
||||
DivCount = rnd.Next(150, 900),
|
||||
SpanCount = rnd.Next(80, 400),
|
||||
ScriptCount = rnd.Next(4, 25),
|
||||
StyleCount = rnd.Next(2, 10),
|
||||
ImgCount = rnd.Next(5, 35),
|
||||
SvgCount = rnd.Next(10, 45),
|
||||
ButtonCount = rnd.Next(6, 30),
|
||||
FormCount = rnd.Next(1, 4),
|
||||
SemanticRatio = Math.Round(0.4 + rnd.NextDouble() * 0.4, 2)
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = rnd.Next(350_000, 2_800_000),
|
||||
HtmlBytes = 65_000,
|
||||
CssBytes = 220_000,
|
||||
JsBytes = 650_000,
|
||||
ImageBytes = 450_000,
|
||||
FontBytes = 90_000
|
||||
},
|
||||
DetectedFonts = ["Inter", "Roboto", "sans-serif"],
|
||||
DetectedTrackers = ["Google Analytics 4"],
|
||||
ThirdPartyDomains = [domain]
|
||||
};
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private static string NormalizeUrl(string input)
|
||||
{
|
||||
var trimmed = input.Trim();
|
||||
if (!trimmed.StartsWith("http://") && !trimmed.StartsWith("https://"))
|
||||
{
|
||||
trimmed = "https://" + trimmed;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private static string GenerateSequence(int pA, int pT, int pC, int pG, int length)
|
||||
{
|
||||
var rnd = new Random(pA * 17 + pT * 11 + pC * 5 + pG);
|
||||
var chars = new char[length];
|
||||
var total = pA + pT + pC + pG;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var roll = rnd.Next(total);
|
||||
if (roll < pA) chars[i] = 'A';
|
||||
else if (roll < pA + pT) chars[i] = 'T';
|
||||
else if (roll < pA + pT + pC) chars[i] = 'C';
|
||||
else chars[i] = 'G';
|
||||
}
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WebsiteDna.Shared\WebsiteDna.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WebsiteDna.Shared;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum OrganismArchetype
|
||||
{
|
||||
MinimalistCrystal = 0, // Pure, lightweight, surgical (e.g. Google, simple landing pages)
|
||||
TextualCoral = 1, // Content-dense, structured reading, high semantic HTML (e.g. Wikipedia, HN)
|
||||
NeuralHyperWeb = 2, // Dynamic, sprawling interactive community (e.g. Reddit, GitHub)
|
||||
MediaLeviathan = 3, // Asset-heavy, rich media, video/canvas canvas (e.g. YouTube, Netflix)
|
||||
DesignSynthesizer = 4, // High-polish, artistic CSS, fluid typography (e.g. Stripe, Apple)
|
||||
BloatedChimera = 5 // Heavy ad-laden, 100+ tracker mutations, unoptimized assets
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum GeneticRarity
|
||||
{
|
||||
Common = 0,
|
||||
Uncommon = 1,
|
||||
Rare = 2,
|
||||
Epic = 3,
|
||||
Legendary = 4,
|
||||
Mythic = 5
|
||||
}
|
||||
|
||||
public class ColorBandDto
|
||||
{
|
||||
public string Hex { get; set; } = "#38bdf8";
|
||||
public double Percentage { get; set; } = 25.0;
|
||||
public string Role { get; set; } = "Primary";
|
||||
}
|
||||
|
||||
public class DomMetricsDto
|
||||
{
|
||||
public int TotalNodes { get; set; }
|
||||
public int MaxDepth { get; set; }
|
||||
public int SemanticElements { get; set; }
|
||||
public int DivCount { get; set; }
|
||||
public int SpanCount { get; set; }
|
||||
public int ScriptCount { get; set; }
|
||||
public int StyleCount { get; set; }
|
||||
public int ImgCount { get; set; }
|
||||
public int SvgCount { get; set; }
|
||||
public int ButtonCount { get; set; }
|
||||
public int FormCount { get; set; }
|
||||
public double SemanticRatio { get; set; }
|
||||
}
|
||||
|
||||
public class AssetFootprintDto
|
||||
{
|
||||
public long TotalBytes { get; set; }
|
||||
public long HtmlBytes { get; set; }
|
||||
public long CssBytes { get; set; }
|
||||
public long JsBytes { get; set; }
|
||||
public long ImageBytes { get; set; }
|
||||
public long FontBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,441 @@
|
||||
namespace WebsiteDna.Shared;
|
||||
|
||||
public static class WebsiteDnaPresets
|
||||
{
|
||||
public static List<WebsiteDnaProfile> GetPresets()
|
||||
{
|
||||
return
|
||||
[
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_google",
|
||||
Url = "https://www.google.com",
|
||||
Domain = "google.com",
|
||||
Title = "Google Search",
|
||||
FaviconUrl = "https://www.google.com/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.MinimalistCrystal,
|
||||
Rarity = GeneticRarity.Mythic,
|
||||
PurityScore = 98.4,
|
||||
ComplexityScore = 18.2,
|
||||
MutationScore = 4.5,
|
||||
GeneticSummary = "A pristine minimalist organism with surgical 4-band chromatics and diamond laser facets. Ultra-dense structural efficiency.",
|
||||
GeneticSequence = GenerateSequence(45, 15, 25, 15, 120),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#4285f4", Percentage = 35, Role = "Google Blue" },
|
||||
new ColorBandDto { Hex = "#ea4335", Percentage = 25, Role = "Google Red" },
|
||||
new ColorBandDto { Hex = "#fbbc05", Percentage = 20, Role = "Google Yellow" },
|
||||
new ColorBandDto { Hex = "#34a853", Percentage = 20, Role = "Google Green" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 54 }, { "T", 18 }, { "C", 30 }, { "G", 18 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 142,
|
||||
MaxDepth = 8,
|
||||
SemanticElements = 24,
|
||||
DivCount = 38,
|
||||
SpanCount = 28,
|
||||
ScriptCount = 4,
|
||||
StyleCount = 2,
|
||||
ImgCount = 3,
|
||||
SvgCount = 6,
|
||||
ButtonCount = 4,
|
||||
FormCount = 1,
|
||||
SemanticRatio = 0.68
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 184_000,
|
||||
HtmlBytes = 42_000,
|
||||
CssBytes = 18_000,
|
||||
JsBytes = 94_000,
|
||||
ImageBytes = 22_000,
|
||||
FontBytes = 8_000
|
||||
},
|
||||
DetectedFonts = ["Roboto", "Google Sans", "Arial"],
|
||||
DetectedTrackers = ["Google Analytics 4"],
|
||||
ThirdPartyDomains = ["gstatic.com", "google.com"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_reddit",
|
||||
Url = "https://www.reddit.com",
|
||||
Domain = "reddit.com",
|
||||
Title = "Reddit - Dive into anything",
|
||||
FaviconUrl = "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.NeuralHyperWeb,
|
||||
Rarity = GeneticRarity.Epic,
|
||||
PurityScore = 72.5,
|
||||
ComplexityScore = 94.0,
|
||||
MutationScore = 48.0,
|
||||
GeneticSummary = "A sprawling, hyper-dense neural organism. Thousands of nested interactive branch nodes with pulsating synaptic electric arcs.",
|
||||
GeneticSequence = GenerateSequence(20, 25, 40, 15, 160),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#ff4500", Percentage = 45, Role = "Reddit Orange" },
|
||||
new ColorBandDto { Hex = "#24a0ed", Percentage = 25, Role = "Upvote Cyan" },
|
||||
new ColorBandDto { Hex = "#d7dadc", Percentage = 15, Role = "Content Slate" },
|
||||
new ColorBandDto { Hex = "#1a1a1b", Percentage = 15, Role = "Deep Void" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 32 }, { "T", 40 }, { "C", 64 }, { "G", 24 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 3400,
|
||||
MaxDepth = 32,
|
||||
SemanticElements = 120,
|
||||
DivCount = 1850,
|
||||
SpanCount = 680,
|
||||
ScriptCount = 38,
|
||||
StyleCount = 12,
|
||||
ImgCount = 45,
|
||||
SvgCount = 88,
|
||||
ButtonCount = 64,
|
||||
FormCount = 4,
|
||||
SemanticRatio = 0.35
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 2_850_000,
|
||||
HtmlBytes = 320_000,
|
||||
CssBytes = 410_000,
|
||||
JsBytes = 1_420_000,
|
||||
ImageBytes = 550_000,
|
||||
FontBytes = 150_000
|
||||
},
|
||||
DetectedFonts = ["IBMPlexSans", "Noto Sans", "system-ui"],
|
||||
DetectedTrackers = ["Google Tag Manager", "Branch Metrics", "Reddit Pixel", "Sentry"],
|
||||
ThirdPartyDomains = ["redditstatic.com", "redditmedia.com", "google-analytics.com", "branch.io"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_stripe",
|
||||
Url = "https://stripe.com",
|
||||
Domain = "stripe.com",
|
||||
Title = "Stripe | Financial Infrastructure for the Internet",
|
||||
FaviconUrl = "https://stripe.com/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.DesignSynthesizer,
|
||||
Rarity = GeneticRarity.Mythic,
|
||||
PurityScore = 94.0,
|
||||
ComplexityScore = 88.0,
|
||||
MutationScore = 12.0,
|
||||
GeneticSummary = "A masterclass in modern generative design synthesis. Fluid multi-spectral color ribbons with metallic gradient caustics.",
|
||||
GeneticSequence = GenerateSequence(30, 40, 20, 10, 150),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#635bff", Percentage = 40, Role = "Stripe Blurple" },
|
||||
new ColorBandDto { Hex = "#00d4ff", Percentage = 25, Role = "Electric Cyan" },
|
||||
new ColorBandDto { Hex = "#7a73ff", Percentage = 20, Role = "Neon Iris" },
|
||||
new ColorBandDto { Hex = "#0a2540", Percentage = 15, Role = "Midnight Navy" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 45 }, { "T", 60 }, { "C", 30 }, { "G", 15 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 1820,
|
||||
MaxDepth = 18,
|
||||
SemanticElements = 210,
|
||||
DivCount = 740,
|
||||
SpanCount = 380,
|
||||
ScriptCount = 14,
|
||||
StyleCount = 6,
|
||||
ImgCount = 14,
|
||||
SvgCount = 54,
|
||||
ButtonCount = 28,
|
||||
FormCount = 2,
|
||||
SemanticRatio = 0.72
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 1_450_000,
|
||||
HtmlBytes = 120_000,
|
||||
CssBytes = 310_000,
|
||||
JsBytes = 680_000,
|
||||
ImageBytes = 220_000,
|
||||
FontBytes = 120_000
|
||||
},
|
||||
DetectedFonts = ["Sohne", "Ideal Sans", "system-ui"],
|
||||
DetectedTrackers = ["Segment.io", "Google Analytics 4"],
|
||||
ThirdPartyDomains = ["stripe.com", "segment.com"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_wikipedia",
|
||||
Url = "https://www.wikipedia.org",
|
||||
Domain = "wikipedia.org",
|
||||
Title = "Wikipedia, the free encyclopedia",
|
||||
FaviconUrl = "https://en.wikipedia.org/static/favicon/wikipedia.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.TextualCoral,
|
||||
Rarity = GeneticRarity.Legendary,
|
||||
PurityScore = 96.0,
|
||||
ComplexityScore = 42.0,
|
||||
MutationScore = 1.0,
|
||||
GeneticSummary = "Ancient textual reef organism. Dense horizontal parchment strata and encyclopedic text density with zero tracker mutations.",
|
||||
GeneticSequence = GenerateSequence(60, 20, 10, 10, 140),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#ffffff", Percentage = 45, Role = "Parchment White" },
|
||||
new ColorBandDto { Hex = "#202122", Percentage = 30, Role = "Obsidian Text" },
|
||||
new ColorBandDto { Hex = "#3366cc", Percentage = 15, Role = "Reference Cobalt" },
|
||||
new ColorBandDto { Hex = "#a2a9b1", Percentage = 10, Role = "Border Slate" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 84 }, { "T", 28 }, { "C", 14 }, { "G", 14 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 1250,
|
||||
MaxDepth = 14,
|
||||
SemanticElements = 310,
|
||||
DivCount = 180,
|
||||
SpanCount = 420,
|
||||
ScriptCount = 3,
|
||||
StyleCount = 4,
|
||||
ImgCount = 18,
|
||||
SvgCount = 12,
|
||||
ButtonCount = 8,
|
||||
FormCount = 1,
|
||||
SemanticRatio = 0.82
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 295_000,
|
||||
HtmlBytes = 110_000,
|
||||
CssBytes = 65_000,
|
||||
JsBytes = 45_000,
|
||||
ImageBytes = 55_000,
|
||||
FontBytes = 20_000
|
||||
},
|
||||
DetectedFonts = ["Linux Libertine", "Hoefler Text", "Georgia", "sans-serif"],
|
||||
DetectedTrackers = [],
|
||||
ThirdPartyDomains = ["wikimedia.org"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_youtube",
|
||||
Url = "https://www.youtube.com",
|
||||
Domain = "youtube.com",
|
||||
Title = "YouTube - Video Platform",
|
||||
FaviconUrl = "https://www.youtube.com/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.MediaLeviathan,
|
||||
Rarity = GeneticRarity.Epic,
|
||||
PurityScore = 78.0,
|
||||
ComplexityScore = 96.0,
|
||||
MutationScore = 32.0,
|
||||
GeneticSummary = "High-energy media leviathan with a massive central gravitational singularity and orbiting video/stream organelles.",
|
||||
GeneticSequence = GenerateSequence(15, 20, 35, 30, 160),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#ff0000", Percentage = 45, Role = "YouTube Crimson" },
|
||||
new ColorBandDto { Hex = "#282828", Percentage = 25, Role = "Dark Carbon" },
|
||||
new ColorBandDto { Hex = "#ffffff", Percentage = 20, Role = "Pure White" },
|
||||
new ColorBandDto { Hex = "#3ea6ff", Percentage = 10, Role = "Link Cyan" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 24 }, { "T", 32 }, { "C", 56 }, { "G", 48 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 4200,
|
||||
MaxDepth = 35,
|
||||
SemanticElements = 90,
|
||||
DivCount = 2400,
|
||||
SpanCount = 980,
|
||||
ScriptCount = 42,
|
||||
StyleCount = 18,
|
||||
ImgCount = 85,
|
||||
SvgCount = 110,
|
||||
ButtonCount = 80,
|
||||
FormCount = 3,
|
||||
SemanticRatio = 0.28
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 5_800_000,
|
||||
HtmlBytes = 450_000,
|
||||
CssBytes = 620_000,
|
||||
JsBytes = 2_800_000,
|
||||
ImageBytes = 1_800_000,
|
||||
FontBytes = 130_000
|
||||
},
|
||||
DetectedFonts = ["YouTube Sans", "Roboto", "Arial"],
|
||||
DetectedTrackers = ["Google Tag Manager", "DoubleClick AdSense"],
|
||||
ThirdPartyDomains = ["ytimg.com", "googlevideo.com", "google.com"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_apple",
|
||||
Url = "https://www.apple.com",
|
||||
Domain = "apple.com",
|
||||
Title = "Apple (United States)",
|
||||
FaviconUrl = "https://www.apple.com/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.DesignSynthesizer,
|
||||
Rarity = GeneticRarity.Legendary,
|
||||
PurityScore = 91.0,
|
||||
ComplexityScore = 82.0,
|
||||
MutationScore = 6.0,
|
||||
GeneticSummary = "Surgical precision typography with titanium monochromatic gradients and piercing Cupertino blue accents.",
|
||||
GeneticSequence = GenerateSequence(35, 30, 20, 15, 140),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#0071e3", Percentage = 35, Role = "Cupertino Blue" },
|
||||
new ColorBandDto { Hex = "#f5f5f7", Percentage = 30, Role = "Titanium Light" },
|
||||
new ColorBandDto { Hex = "#86868b", Percentage = 20, Role = "Graphite Slate" },
|
||||
new ColorBandDto { Hex = "#000000", Percentage = 15, Role = "Obsidian Space" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 49 }, { "T", 42 }, { "C", 28 }, { "G", 21 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 1650,
|
||||
MaxDepth = 16,
|
||||
SemanticElements = 190,
|
||||
DivCount = 680,
|
||||
SpanCount = 310,
|
||||
ScriptCount = 16,
|
||||
StyleCount = 8,
|
||||
ImgCount = 32,
|
||||
SvgCount = 42,
|
||||
ButtonCount = 18,
|
||||
FormCount = 1,
|
||||
SemanticRatio = 0.65
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 3_200_000,
|
||||
HtmlBytes = 140_000,
|
||||
CssBytes = 390_000,
|
||||
JsBytes = 850_000,
|
||||
ImageBytes = 1_620_000,
|
||||
FontBytes = 200_000
|
||||
},
|
||||
DetectedFonts = ["SF Pro Display", "SF Pro Text", "SF Pro Icons"],
|
||||
DetectedTrackers = ["Apple Telemetry"],
|
||||
ThirdPartyDomains = ["apple.com", "apple-mapkit.com"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_chimera",
|
||||
Url = "https://www.adtracker-portal.net",
|
||||
Domain = "adtracker-portal.net",
|
||||
Title = "Toxic Mutant Ad Portal",
|
||||
FaviconUrl = "https://www.google.com/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.BloatedChimera,
|
||||
Rarity = GeneticRarity.Common,
|
||||
PurityScore = 34.0,
|
||||
ComplexityScore = 98.0,
|
||||
MutationScore = 88.0,
|
||||
GeneticSummary = "Severely mutated bio-structure with 40+ parasitic trackers, 12MB uncompressed assets, and erratic jagged bone rungs.",
|
||||
GeneticSequence = GenerateSequence(10, 15, 60, 15, 150),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#ef4444", Percentage = 45, Role = "Toxic Crimson" },
|
||||
new ColorBandDto { Hex = "#10b981", Percentage = 30, Role = "Mutagen Green" },
|
||||
new ColorBandDto { Hex = "#f59e0b", Percentage = 15, Role = "Warning Amber" },
|
||||
new ColorBandDto { Hex = "#7f1d1d", Percentage = 10, Role = "Necrotic Marroon" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 15 }, { "T", 22 }, { "C", 90 }, { "G", 23 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 5200,
|
||||
MaxDepth = 42,
|
||||
SemanticElements = 30,
|
||||
DivCount = 3800,
|
||||
SpanCount = 920,
|
||||
ScriptCount = 82,
|
||||
StyleCount = 24,
|
||||
ImgCount = 120,
|
||||
SvgCount = 15,
|
||||
ButtonCount = 45,
|
||||
FormCount = 8,
|
||||
SemanticRatio = 0.12
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 11_400_000,
|
||||
HtmlBytes = 680_000,
|
||||
CssBytes = 920_000,
|
||||
JsBytes = 4_800_000,
|
||||
ImageBytes = 4_800_000,
|
||||
FontBytes = 200_000
|
||||
},
|
||||
DetectedFonts = ["Arial", "Comic Sans MS", "Impact"],
|
||||
DetectedTrackers = ["Google Tag Manager", "Meta Pixel", "Hotjar", "Criteo", "Outbrain", "Taboola", "Yandex Metrika", "TikTok Pixel"],
|
||||
ThirdPartyDomains = ["criteo.com", "outbrain.com", "taboola.com", "doubleclick.net", "facebook.com"]
|
||||
},
|
||||
|
||||
new WebsiteDnaProfile
|
||||
{
|
||||
Id = "preset_hackernews",
|
||||
Url = "https://news.ycombinator.com",
|
||||
Domain = "news.ycombinator.com",
|
||||
Title = "Hacker News",
|
||||
FaviconUrl = "https://news.ycombinator.com/favicon.ico",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
Archetype = OrganismArchetype.TextualCoral,
|
||||
Rarity = GeneticRarity.Rare,
|
||||
PurityScore = 92.0,
|
||||
ComplexityScore = 15.0,
|
||||
MutationScore = 0.0,
|
||||
GeneticSummary = "Primitive fossil organism. Pure table-based textual lattice untouched by modern script mutations since the dawn of the web.",
|
||||
GeneticSequence = GenerateSequence(70, 15, 5, 10, 100),
|
||||
ColorBands =
|
||||
[
|
||||
new ColorBandDto { Hex = "#ff6600", Percentage = 35, Role = "Y-Combinator Orange" },
|
||||
new ColorBandDto { Hex = "#f6f6ef", Percentage = 45, Role = "Newsprint Cream" },
|
||||
new ColorBandDto { Hex = "#828282", Percentage = 20, Role = "Monochrome Text" }
|
||||
],
|
||||
BaseCounts = new() { { "A", 70 }, { "T", 15 }, { "C", 5 }, { "G", 10 } },
|
||||
DomMetrics = new DomMetricsDto
|
||||
{
|
||||
TotalNodes = 320,
|
||||
MaxDepth = 7,
|
||||
SemanticElements = 15,
|
||||
DivCount = 12,
|
||||
SpanCount = 45,
|
||||
ScriptCount = 1,
|
||||
StyleCount = 1,
|
||||
ImgCount = 4,
|
||||
SvgCount = 0,
|
||||
ButtonCount = 2,
|
||||
FormCount = 2,
|
||||
SemanticRatio = 0.5
|
||||
},
|
||||
AssetFootprint = new AssetFootprintDto
|
||||
{
|
||||
TotalBytes = 42_000,
|
||||
HtmlBytes = 32_000,
|
||||
CssBytes = 4_000,
|
||||
JsBytes = 2_000,
|
||||
ImageBytes = 4_000,
|
||||
FontBytes = 0
|
||||
},
|
||||
DetectedFonts = ["Verdana", "Geneva", "sans-serif"],
|
||||
DetectedTrackers = [],
|
||||
ThirdPartyDomains = []
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static string GenerateSequence(int pA, int pT, int pC, int pG, int length)
|
||||
{
|
||||
var rnd = new Random(pA * 13 + pT * 7 + pC * 3 + pG);
|
||||
var chars = new char[length];
|
||||
var total = pA + pT + pC + pG;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var roll = rnd.Next(total);
|
||||
if (roll < pA) chars[i] = 'A';
|
||||
else if (roll < pA + pT) chars[i] = 'T';
|
||||
else if (roll < pA + pT + pC) chars[i] = 'C';
|
||||
else chars[i] = 'G';
|
||||
}
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace WebsiteDna.Shared;
|
||||
|
||||
public class WebsiteDnaProfile
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string FaviconUrl { get; set; } = string.Empty;
|
||||
public DateTimeOffset AnalyzedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public OrganismArchetype Archetype { get; set; }
|
||||
public GeneticRarity Rarity { get; set; }
|
||||
|
||||
public double PurityScore { get; set; } // 0 - 100
|
||||
public double ComplexityScore { get; set; } // 0 - 100
|
||||
public double MutationScore { get; set; } // 0 - 100
|
||||
|
||||
public string GeneticSummary { get; set; } = string.Empty;
|
||||
public string GeneticSequence { get; set; } = string.Empty; // e.g. "AATCGTCGAACCGT..."
|
||||
|
||||
public List<ColorBandDto> ColorBands { get; set; } = [];
|
||||
public Dictionary<string, int> BaseCounts { get; set; } = new();
|
||||
public DomMetricsDto DomMetrics { get; set; } = new();
|
||||
public AssetFootprintDto AssetFootprint { get; set; } = new();
|
||||
|
||||
public List<string> DetectedFonts { get; set; } = [];
|
||||
public List<string> DetectedTrackers { get; set; } = [];
|
||||
public List<string> ThirdPartyDomains { get; set; } = [];
|
||||
}
|
||||
|
||||
public class AnalysisRequest
|
||||
{
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CompareRequest
|
||||
{
|
||||
public string Url1 { get; set; } = string.Empty;
|
||||
public string Url2 { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ComparisonResult
|
||||
{
|
||||
public WebsiteDnaProfile ProfileA { get; set; } = new();
|
||||
public WebsiteDnaProfile ProfileB { get; set; } = new();
|
||||
public double GeneticSimilarityPercent { get; set; }
|
||||
public string CompatibilityVerdict { get; set; } = string.Empty;
|
||||
}
|
||||
Reference in New Issue
Block a user