From 1e8b7082a4d6ffe12e1b53f18b1cb1ec67c1f8a4 Mon Sep 17 00:00:00 2001 From: default Date: Sat, 24 Jan 2026 21:30:08 +0000 Subject: [PATCH] UI Rework --- frontend/package.json | 5 +- frontend/src/app.css | 1 + frontend/src/app.html | 24 +- .../src/lib/components/CameraScreen.svelte | 381 ++++---- .../src/lib/components/CloudSection.svelte | 11 +- .../src/lib/components/CustomTabBar.svelte | 287 ++---- frontend/src/lib/components/HomeScreen.svelte | 465 ++++++--- .../src/lib/components/MobileHomePage.svelte | 533 +++++++++++ .../src/lib/components/MobileTabBar.svelte | 24 - frontend/src/lib/components/OnlyMobile.svelte | 17 - frontend/src/lib/components/OnlyWeb.svelte | 17 - .../lib/components/ParallaxLandscape.svelte | 385 +++++--- .../lib/components/ProductCarousel3D.svelte | 234 ----- .../src/lib/components/ThreeBackground.svelte | 174 ---- frontend/src/lib/components/WebFooter.svelte | 12 - .../src/lib/components/WebHomePage.svelte | 650 +++++++++++++ frontend/src/lib/components/WebNavbar.svelte | 23 - frontend/src/lib/ts/parallax/colors.ts | 182 ++++ frontend/src/lib/ts/parallax/drawBase.ts | 151 +++ frontend/src/lib/ts/parallax/index.ts | 95 ++ frontend/src/lib/ts/parallax/scenes/city.ts | 189 ++++ .../lib/ts/parallax/scenes/deforestation.ts | 137 +++ frontend/src/lib/ts/parallax/scenes/eco.ts | 145 +++ frontend/src/lib/ts/parallax/scenes/forest.ts | 168 ++++ .../src/lib/ts/parallax/scenes/industrial.ts | 130 +++ frontend/src/lib/ts/parallax/scenes/ocean.ts | 197 ++++ frontend/src/lib/ts/parallax/scenes/oilRig.ts | 162 ++++ .../lib/ts/parallax/scenes/pollutedCity.ts | 176 ++++ frontend/src/lib/ts/parallax/types.ts | 44 + frontend/src/routes/+layout.svelte | 189 ++-- frontend/src/routes/+page.svelte | 892 +----------------- frontend/src/routes/catalogue/+page.svelte | 245 +++-- frontend/src/routes/chat/+page.svelte | 331 ++++--- frontend/src/routes/community/+page.svelte | 290 +++--- frontend/src/routes/news/+page.svelte | 200 +++- frontend/src/routes/report/+page.svelte | 232 ++--- frontend/vite.config.js | 4 +- 37 files changed, 4648 insertions(+), 2754 deletions(-) create mode 100644 frontend/src/app.css create mode 100644 frontend/src/lib/components/MobileHomePage.svelte delete mode 100644 frontend/src/lib/components/MobileTabBar.svelte delete mode 100644 frontend/src/lib/components/OnlyMobile.svelte delete mode 100644 frontend/src/lib/components/OnlyWeb.svelte delete mode 100644 frontend/src/lib/components/ProductCarousel3D.svelte delete mode 100644 frontend/src/lib/components/ThreeBackground.svelte delete mode 100644 frontend/src/lib/components/WebFooter.svelte create mode 100644 frontend/src/lib/components/WebHomePage.svelte delete mode 100644 frontend/src/lib/components/WebNavbar.svelte create mode 100644 frontend/src/lib/ts/parallax/colors.ts create mode 100644 frontend/src/lib/ts/parallax/drawBase.ts create mode 100644 frontend/src/lib/ts/parallax/index.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/city.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/deforestation.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/eco.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/forest.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/industrial.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/ocean.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/oilRig.ts create mode 100644 frontend/src/lib/ts/parallax/scenes/pollutedCity.ts create mode 100644 frontend/src/lib/ts/parallax/types.ts diff --git a/frontend/package.json b/frontend/package.json index f6246c1..785f258 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,11 +14,14 @@ }, "license": "MIT", "dependencies": { + "@iconify/svelte": "^5.2.1", + "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-opener": "^2.5.3", "@types/three": "^0.182.0", "compression": "^1.8.1", "express": "^5.2.1", + "tailwindcss": "^4.1.18", "three": "^0.182.0" }, "devDependencies": { @@ -26,7 +29,7 @@ "@sveltejs/kit": "^2.50.1", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tauri-apps/cli": "^2.9.6", - "svelte": "^5.48.0", + "svelte": "^5.48.2", "svelte-check": "^4.3.5", "typescript": "~5.6.3", "vite": "^6.4.1" diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..a461c50 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/frontend/src/app.html b/frontend/src/app.html index 92e7e33..1ccbb4a 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,13 +1,15 @@ - - - - - Tauri + SvelteKit + Typescript App - %sveltekit.head% - - -
%sveltekit.body%
- - + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + + \ No newline at end of file diff --git a/frontend/src/lib/components/CameraScreen.svelte b/frontend/src/lib/components/CameraScreen.svelte index 4ee675a..eccb2ba 100644 --- a/frontend/src/lib/components/CameraScreen.svelte +++ b/frontend/src/lib/components/CameraScreen.svelte @@ -1,5 +1,6 @@
- - - -
- - - - import { goto } from "$app/navigation"; + import Icon from "@iconify/svelte"; interface Props { currentRoute: string; @@ -9,139 +10,88 @@ const tabs = [ { - name: "Mission", + name: "Goals", route: "/community", - icon: "people", - activeColor: "#f472b6", + icon: "ri:flag-fill", + activeColor: "#34d399", }, { name: "History", - route: "/", - icon: "time", - activeColor: "#1ed760", + route: "/", + icon: "ri:time-fill", + activeColor: "#34d399", }, { - name: "Home", - route: "/", // Hidden - for center button only - icon: "home", - activeColor: "#4ade80", + name: "Scan", + route: "/", + icon: "ri:camera-lens-fill", isCenter: true, }, { name: "Report", route: "/report", - icon: "megaphone", + icon: "ri:alarm-warning-fill", activeColor: "#ef4444", }, { - name: "Ask AI", + name: "Chat", route: "/chat", - icon: "chatbubble-ellipses", - activeColor: "#4ade80", + icon: "ri:chat-3-fill", + activeColor: "#60a5fa", }, ]; - function navigateToTab(route: string) { goto(route); } - function getIconPath(icon: string, isFocused: boolean): string { - switch (icon) { - case "people": - return isFocused - ? "M166.5 83.5C148.6 98.07 128 111.1 104.1 123.5C119.9 145.9 131.8 171.9 138.5 200h98.7C226.5 138.5 192 88.96 146.5 63.36C154.2 71.48 161.6 77.25 166.5 83.5zM196.7 368H159.3c6.672 28.11 18.53 54.08 34.39 76.52C209.5 422.1 226.1 393.3 237.3 368zM104.1 388.5C128 400.9 148.6 413.9 166.5 428.5C161.6 434.8 154.2 440.5 146.5 448.6C155.2 456.8 166.5 462.5 179.2 464.1C184.1 456.2 192.5 447.7 203.5 437.5C221.4 422.9 242 409.9 265.9 397.5C250.1 375.1 238.2 349.1 231.5 320H116.5C109.8 348.1 97.94 374.1 82.14 396.5zM365.9 248C359.2 219.9 347.3 193.9 331.5 171.5C307.6 183.9 287 196.9 269.1 211.5C274 217.8 281.4 223.5 289.1 231.6C280.4 239.8 269.1 245.5 256.4 247.1C251.5 239.2 243.1 230.7 232.1 220.5C214.2 205.9 193.6 192.9 169.7 180.5C185.5 158.1 197.4 132.1 204.1 104h114.1c6.672 28.11 18.53 54.08 34.39 76.52C368.5 158.1 389.1 145.1 407 130.5C402.1 124.2 394.6 118.5 386.9 110.4C395.6 102.2 406.9 96.54 419.6 94.93C424.5 102.8 432.9 111.3 443.9 121.5C461.8 136.1 482.4 149.1 506.3 161.5C490.5 183.9 478.6 209.9 471.9 238h-98.66C380.1 269.5 391.9 295.5 407.7 317.9C383.8 330.3 363.2 343.3 345.3 357.9C350.2 364.2 357.6 369.9 365.3 378C356.6 386.2 345.3 391.9 332.6 393.5C327.7 385.6 319.3 377.1 308.3 366.9C290.4 352.3 269.8 339.3 245.9 326.9C226.1 304.5 213.8 278.5 207.1 248h158.8z M288 0C422.4 0 512 89.6 512 224S422.4 448 288 448S64 358.4 64 224S153.6 0 288 0z" - : "M166.5 83.5C148.6 98.07 128 111.1 104.1 123.5C119.9 145.9 131.8 171.9 138.5 200h98.7C226.5 138.5 192 88.96 146.5 63.36C154.2 71.48 161.6 77.25 166.5 83.5zM196.7 368H159.3c6.672 28.11 18.53 54.08 34.39 76.52C209.5 422.1 226.1 393.3 237.3 368zM104.1 388.5C128 400.9 148.6 413.9 166.5 428.5C161.6 434.8 154.2 440.5 146.5 448.6C155.2 456.8 166.5 462.5 179.2 464.1C184.1 456.2 192.5 447.7 203.5 437.5C221.4 422.9 242 409.9 265.9 397.5C250.1 375.1 238.2 349.1 231.5 320H116.5C109.8 348.1 97.94 374.1 82.14 396.5zM365.9 248C359.2 219.9 347.3 193.9 331.5 171.5C307.6 183.9 287 196.9 269.1 211.5C274 217.8 281.4 223.5 289.1 231.6C280.4 239.8 269.1 245.5 256.4 247.1C251.5 239.2 243.1 230.7 232.1 220.5C214.2 205.9 193.6 192.9 169.7 180.5C185.5 158.1 197.4 132.1 204.1 104h114.1c6.672 28.11 18.53 54.08 34.39 76.52C368.5 158.1 389.1 145.1 407 130.5C402.1 124.2 394.6 118.5 386.9 110.4C395.6 102.2 406.9 96.54 419.6 94.93C424.5 102.8 432.9 111.3 443.9 121.5C461.8 136.1 482.4 149.1 506.3 161.5C490.5 183.9 478.6 209.9 471.9 238h-98.66C380.1 269.5 391.9 295.5 407.7 317.9C383.8 330.3 363.2 343.3 345.3 357.9C350.2 364.2 357.6 369.9 365.3 378C356.6 386.2 345.3 391.9 332.6 393.5C327.7 385.6 319.3 377.1 308.3 366.9C290.4 352.3 269.8 339.3 245.9 326.9C226.1 304.5 213.8 278.5 207.1 248h158.8z M288 0C422.4 0 512 89.6 512 224S422.4 448 288 448S64 358.4 64 224S153.6 0 288 0z"; - case "time": - return isFocused - ? "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z" - : "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"; - case "newspaper": - return isFocused - ? "M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z" - : "M96 96c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H80c-44.2 0-80-35.8-80-80V128c0-17.7 14.3-32 32-32s32 14.3 32 32V400c0 8.8 7.2 16 16 16s16-7.2 16-16V96zm64 24v80c0 13.3 10.7 24 24 24H296c13.3 0 24-10.7 24-24V120c0-13.3-10.7-24-24-24H184c-13.3 0-24 10.7-24 24zm128 0v80H192V120H288zM160 280c0-13.3 10.7-24 24-24H424c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24zm0 80c0-13.3 10.7-24 24-24H424c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24z"; - case "megaphone": - return isFocused - ? "M480 32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9L381.7 53c-48 48-113.1 75-181 75H192 160 64c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64l0 128c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V352l8.7 0c67.9 0 133 27 181 75l43.6 43.6c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V300.4c18.6-8.8 32-32.5 32-60.4s-13.4-51.6-32-60.4V32zm-64 76.7V240 371.3C357.2 317.8 280.5 288 200.7 288H192V192h8.7c79.8 0 156.5-29.8 215.3-83.3z" - : "M480 32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9L381.7 53c-48 48-113.1 75-181 75H192 160 64c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64l0 128c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V352l8.7 0c67.9 0 133 27 181 75l43.6 43.6c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V300.4c18.6-8.8 32-32.5 32-60.4s-13.4-51.6-32-60.4V32zM288 240c0 8.8-7.2 16-16 16H192V192h80c8.8 0 16 7.2 16 16z"; - case "chatbubble-ellipses": - return isFocused - ? "M256 448c141.4 0 256-93.1 256-208S397.4 32 256 32S0 125.1 0 240c0 45.1 17.7 86.8 47.7 120.9c-1.9 24.5-11.4 46.3-21.4 62.9c-5.5 9.2-11.1 16.6-15.2 21.6c-2.1 2.5-3.7 4.4-4.9 5.7c-.6 .6-1 1.1-1.3 1.4l-.3 .3 0 0 0 0 0 0 0 0c-4.6 4.6-5.9 11.4-3.4 17.4c2.5 6 8.3 9.9 14.8 9.9c28.7 0 57.6-8.9 81.6-19.3c22.9-10 42.4-21.9 54.3-30.6c31.8 11.5 67 17.9 104.1 17.9zM128 208a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm128 0a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm96 32a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z" - : "M256 448c141.4 0 256-93.1 256-208S397.4 32 256 32S0 125.1 0 240c0 45.1 17.7 86.8 47.7 120.9c-1.9 24.5-11.4 46.3-21.4 62.9c-5.5 9.2-11.1 16.6-15.2 21.6c-2.1 2.5-3.7 4.4-4.9 5.7c-.6 .6-1 1.1-1.3 1.4l-.3 .3 0 0 0 0 0 0 0 0c-4.6 4.6-5.9 11.4-3.4 17.4c2.5 6 8.3 9.9 14.8 9.9c28.7 0 57.6-8.9 81.6-19.3c22.9-10 42.4-21.9 54.3-30.6c31.8 11.5 67 17.9 104.1 17.9zm24-240a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm-72 0a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm-72 0a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"; - default: - return ""; - } - } - function handleScan() { - // This will be handled by parent const event = new CustomEvent("scan"); window.dispatchEvent(event); }
-
-
- {#each tabs as tab, index} - {#if !tab.isCenter} - - {/if} - {#if index === 1} -
- {/if} - {/each} -
-
- - -
-
- -
+ + {tab.name} + + + {/if} + {#if index === 1} + + {/if} + {/each}
@@ -151,130 +101,55 @@ bottom: 0; left: 0; right: 0; - padding-bottom: 20px; - padding-left: 12px; - padding-right: 12px; + padding: 0; z-index: 100; - pointer-events: none; } .tab-bar { - height: 85px; - border-radius: 32px; - background: rgba(30, 41, 59, 0.8); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06), - 0 20px 25px -5px rgba(0, 0, 0, 0.4); - border: 1px solid rgba(255, 255, 255, 0.08); - border-top: 1.5px solid rgba(255, 255, 255, 0.15); - pointer-events: all; - } - - .tab-bar-inner { display: flex; - height: 100%; align-items: center; - padding: 0 16px; + justify-content: space-around; + height: 80px; + background: #0d2e25; + border-top: 1px solid #1f473b; + padding: 0 16px 16px; } .tab-item { flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 12px 0; - background: none; - border: none; - cursor: pointer; - transition: all 0.3s ease; - } - - .tab-content { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 8px 12px; - border-radius: 18px; - transition: all 0.3s ease; - } - - .tab-content svg { - width: 26px; - height: 26px; - transition: all 0.3s ease; - } - - .tab-content span { - font-size: 10px; - font-weight: 500; - margin-top: 4px; - letter-spacing: 0.2px; - transition: all 0.3s ease; - } - - .tab-content.active span { - font-weight: 600; - } - - .tab-spacer { - width: 80px; - } - - .center-button-wrapper { - position: absolute; - bottom: 48px; - left: 50%; - transform: translateX(-50%); - pointer-events: all; - } - - .center-button-ring { - position: relative; - width: 90px; - height: 90px; - border-radius: 45px; - background: rgba(74, 222, 128, 0.1); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - border: 1.5px solid rgba(74, 222, 128, 0.3); - display: flex; - align-items: center; - justify-content: center; - } - - .center-button { - width: 65px; - height: 65px; - border-radius: 33px; - background: linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%); - border: 2.5px solid rgba(15, 23, 42, 0.5); + gap: 4px; + background: none; + border: none; cursor: pointer; - transition: all 0.3s ease; + padding-top: 12px; + } + + .tab-name { + font-size: 10px; + font-weight: 600; + transition: color 0.15s; + } + + .scan-button { + width: 56px; + height: 56px; + background: #10b981; + border-radius: 50%; display: flex; align-items: center; justify-content: center; - box-shadow: - 0 12px 20px rgba(74, 222, 128, 0.6), - 0 4px 8px rgba(0, 0, 0, 0.2); + cursor: pointer; + margin-bottom: 24px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + transition: transform 0.15s; + border: 4px solid #051f18; } - .center-button:hover { - transform: scale(1.05); - box-shadow: - 0 16px 24px rgba(74, 222, 128, 0.7), - 0 6px 10px rgba(0, 0, 0, 0.3); - } - - .center-button:active { - transform: scale(0.98); - } - - .center-button svg { - width: 30px; - height: 30px; + .scan-button:active { + transform: scale(0.95); } diff --git a/frontend/src/lib/components/HomeScreen.svelte b/frontend/src/lib/components/HomeScreen.svelte index 2b3a6e1..ad209a6 100644 --- a/frontend/src/lib/components/HomeScreen.svelte +++ b/frontend/src/lib/components/HomeScreen.svelte @@ -1,5 +1,6 @@
+
+
-
-

Ethix

-

Truth in every scan.

+
+
+ +
+
+

Ethix

+

Truth in every scan

+
-
+
+
- +
+
+
+ +
+
+ 47 + Scans +
+
+
+
+
+ +
+
+ 32 + Eco Picks +
+
+
+
+
+ +
+
+ 78% + Score +
+
+
+
-

Recent Activity

+
+

Recent Activity

+ +
+ {#if recentItems.length === 0} -

No recent scans.

+
+ +

No recent scans yet

+ Start scanning to see your history +
{:else} {#each recentItems as item (item.id)} - + + +
+
+ Your Carbon Savings +
+ 23 + kg CO₂ +
+
+ + ≈ driving 92 km less +
+
+
+
+ +
+ 2 trees planted +
+
+ +
+
+
+ +
+ 47 + scans +
+
+
+ +
+ 32 + eco picks +
+
+
+ +
+ 78% + score +
+
+ +
+
+

Recent Scans

+ + See all + + +
+ +
+ {#each scanHistory as item (item.id)} + + {/each} +
+
+ +
+
+ +
+
+ Daily Tip: Bring reusable bags shopping to cut 500 plastic + bags per year! +
+
+
+ + diff --git a/frontend/src/lib/components/MobileTabBar.svelte b/frontend/src/lib/components/MobileTabBar.svelte deleted file mode 100644 index 879b1e1..0000000 --- a/frontend/src/lib/components/MobileTabBar.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/frontend/src/lib/components/OnlyMobile.svelte b/frontend/src/lib/components/OnlyMobile.svelte deleted file mode 100644 index c19853d..0000000 --- a/frontend/src/lib/components/OnlyMobile.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -{#if mounted && isNative} - {@render children()} -{/if} diff --git a/frontend/src/lib/components/OnlyWeb.svelte b/frontend/src/lib/components/OnlyWeb.svelte deleted file mode 100644 index d975b8e..0000000 --- a/frontend/src/lib/components/OnlyWeb.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -{#if mounted && !isNative} - {@render children()} -{/if} diff --git a/frontend/src/lib/components/ParallaxLandscape.svelte b/frontend/src/lib/components/ParallaxLandscape.svelte index 425f708..03abad6 100644 --- a/frontend/src/lib/components/ParallaxLandscape.svelte +++ b/frontend/src/lib/components/ParallaxLandscape.svelte @@ -1,188 +1,267 @@ - - -
- -
-
- - -
-
-
- - -
- - - -
- - -
- - - - - - -
- - -
- - - -
- - -
- - - -
- - -
- - - - - - - - -
+
+
diff --git a/frontend/src/lib/components/ProductCarousel3D.svelte b/frontend/src/lib/components/ProductCarousel3D.svelte deleted file mode 100644 index c382242..0000000 --- a/frontend/src/lib/components/ProductCarousel3D.svelte +++ /dev/null @@ -1,234 +0,0 @@ - - -
- - diff --git a/frontend/src/lib/components/ThreeBackground.svelte b/frontend/src/lib/components/ThreeBackground.svelte deleted file mode 100644 index 99b265a..0000000 --- a/frontend/src/lib/components/ThreeBackground.svelte +++ /dev/null @@ -1,174 +0,0 @@ - - -
- -
- - diff --git a/frontend/src/lib/components/WebFooter.svelte b/frontend/src/lib/components/WebFooter.svelte deleted file mode 100644 index 8c672a7..0000000 --- a/frontend/src/lib/components/WebFooter.svelte +++ /dev/null @@ -1,12 +0,0 @@ -
-

© 2026 My Website. All rights reserved.

-
- - diff --git a/frontend/src/lib/components/WebHomePage.svelte b/frontend/src/lib/components/WebHomePage.svelte new file mode 100644 index 0000000..ab54b30 --- /dev/null +++ b/frontend/src/lib/components/WebHomePage.svelte @@ -0,0 +1,650 @@ + + +
+ + +
+
+
+ + See the real impact +
+

+ Know What
You Buy. +

+

+ Scan a product. See if it's actually good for the planet. Find + better alternatives if it's not. Simple as that. +

+ +
+ +
+
+
+ {#key scoreIndex} +
+ {scores[scoreIndex].label} +
+ {/key} +
+
+
+ +
+ {#key scoreIndex} +
+ +
+
+ {scores[scoreIndex].label} +
+
+ {scores[scoreIndex].score} +
+
+
+ {/key} +
+
+
+
+ +
+ +
+
+ {#each stats as stat} +
+
{stat.value}
+
{stat.label}
+
+ {/each} +
+
+ +
+
+

How It Works

+

Tools to help you shop smarter.

+
+
+ {#each features as feature} +
+
+ +
+

{feature.title}

+

{feature.desc}

+
+ {/each} +
+
+ +
+
+

Latest News

+

+ Updates from the world of sustainability. +

+
+
+ {#each news as item (item.id)} + + {/each} +
+
+ + +
+ + diff --git a/frontend/src/lib/components/WebNavbar.svelte b/frontend/src/lib/components/WebNavbar.svelte deleted file mode 100644 index 2a54978..0000000 --- a/frontend/src/lib/components/WebNavbar.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/frontend/src/lib/ts/parallax/colors.ts b/frontend/src/lib/ts/parallax/colors.ts new file mode 100644 index 0000000..26eadc4 --- /dev/null +++ b/frontend/src/lib/ts/parallax/colors.ts @@ -0,0 +1,182 @@ +import type { SceneColors, SceneType, ParallaxState } from './types'; + +export const SCENE_COLORS: Record, SceneColors> = { + eco: { + skyTop: '#87CEEB', + skyBottom: '#E0F7FA', + sun: '#FFD54F', + sunGlow: 'rgba(255, 213, 79, 0.3)', + mountainFar: '#81C784', + mountainMid: '#66BB6A', + hillFront: '#4CAF50', + treeDark: '#2E7D32', + treeLight: '#66BB6A', + ground: '#8BC34A', + water: '#4FC3F7', + cloud: 'rgba(255, 255, 255, 0.9)', + accent: '#E91E63', + }, + industrial: { + skyTop: '#4A4A4A', + skyBottom: '#757575', + sun: '#FFA726', + sunGlow: 'rgba(255, 167, 38, 0.2)', + mountainFar: '#616161', + mountainMid: '#757575', + hillFront: '#5D4037', + treeDark: '#4E342E', + treeLight: '#6D4C41', + ground: '#795548', + water: '#546E7A', + cloud: 'rgba(120, 120, 120, 0.7)', + accent: '#D32F2F', + }, + forest: { + skyTop: '#64B5F6', + skyBottom: '#B3E5FC', + sun: '#FFD54F', + sunGlow: 'rgba(255, 213, 79, 0.4)', + mountainFar: '#1B5E20', + mountainMid: '#2E7D32', + hillFront: '#388E3C', + treeDark: '#1B5E20', + treeLight: '#4CAF50', + ground: '#33691E', + water: '#26A69A', + cloud: 'rgba(255, 255, 255, 0.85)', + accent: '#8BC34A', + }, + deforestation: { + skyTop: '#8D6E63', + skyBottom: '#BCAAA4', + sun: '#FF8A65', + sunGlow: 'rgba(255, 138, 101, 0.3)', + mountainFar: '#6D4C41', + mountainMid: '#8D6E63', + hillFront: '#A1887F', + treeDark: '#5D4037', + treeLight: '#8D6E63', + ground: '#4E342E', + water: '#78909C', + cloud: 'rgba(180, 160, 140, 0.8)', + accent: '#FF5722', + }, + ocean: { + skyTop: '#039BE5', + skyBottom: '#81D4FA', + sun: '#FFF176', + sunGlow: 'rgba(255, 241, 118, 0.4)', + mountainFar: '#0277BD', + mountainMid: '#0288D1', + hillFront: '#03A9F4', + treeDark: '#00897B', + treeLight: '#26A69A', + ground: '#4DB6AC', + water: '#00ACC1', + cloud: 'rgba(255, 255, 255, 0.95)', + accent: '#00BCD4', + }, + oilRig: { + skyTop: '#37474F', + skyBottom: '#546E7A', + sun: '#FF6F00', + sunGlow: 'rgba(255, 111, 0, 0.25)', + mountainFar: '#263238', + mountainMid: '#37474F', + hillFront: '#455A64', + treeDark: '#263238', + treeLight: '#37474F', + ground: '#1C313A', + water: '#1C313A', + cloud: 'rgba(80, 80, 80, 0.8)', + accent: '#FF5722', + }, + city: { + skyTop: '#42A5F5', + skyBottom: '#90CAF9', + sun: '#FFEB3B', + sunGlow: 'rgba(255, 235, 59, 0.35)', + mountainFar: '#78909C', + mountainMid: '#90A4AE', + hillFront: '#B0BEC5', + treeDark: '#4CAF50', + treeLight: '#81C784', + ground: '#ECEFF1', + water: '#26C6DA', + cloud: 'rgba(255, 255, 255, 0.9)', + accent: '#2196F3', + }, + pollutedCity: { + skyTop: '#424242', + skyBottom: '#616161', + sun: '#EF6C00', + sunGlow: 'rgba(239, 108, 0, 0.2)', + mountainFar: '#424242', + mountainMid: '#616161', + hillFront: '#757575', + treeDark: '#4E342E', + treeLight: '#5D4037', + ground: '#3E2723', + water: '#37474F', + cloud: 'rgba(90, 90, 90, 0.85)', + accent: '#F44336', + }, +}; + +export function lerpColor(color1: string, color2: string, t: number): string { + const parse = (c: string) => { + if (c.startsWith('#')) { + const hex = c.slice(1); + return { + r: parseInt(hex.slice(0, 2), 16), + g: parseInt(hex.slice(2, 4), 16), + b: parseInt(hex.slice(4, 6), 16), + a: 1, + }; + } + const match = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (match) { + return { + r: parseInt(match[1]), + g: parseInt(match[2]), + b: parseInt(match[3]), + a: parseFloat(match[4] ?? '1'), + }; + } + return { r: 0, g: 0, b: 0, a: 1 }; + }; + + const c1 = parse(color1); + const c2 = parse(color2); + + const r = Math.round(c1.r + (c2.r - c1.r) * t); + const g = Math.round(c1.g + (c2.g - c1.g) * t); + const b = Math.round(c1.b + (c2.b - c1.b) * t); + const a = c1.a + (c2.a - c1.a) * t; + + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +function blendColors(colors1: SceneColors, colors2: SceneColors, t: number): SceneColors { + const result = {} as SceneColors; + for (const key of Object.keys(colors1) as (keyof SceneColors)[]) { + result[key] = lerpColor(colors1[key], colors2[key], t); + } + return result; +} + +export function getSceneColors(state: ParallaxState): SceneColors { + const { sceneType, progress, blendToScene, blendProgress } = state; + + if (sceneType === 'transition') { + return blendColors(SCENE_COLORS.eco, SCENE_COLORS.industrial, progress); + } + + if (blendToScene && blendProgress !== undefined && blendToScene !== 'transition') { + const fromColors = SCENE_COLORS[sceneType as keyof typeof SCENE_COLORS] || SCENE_COLORS.eco; + const toColors = SCENE_COLORS[blendToScene as keyof typeof SCENE_COLORS] || SCENE_COLORS.eco; + return blendColors(fromColors, toColors, blendProgress); + } + + return SCENE_COLORS[sceneType as keyof typeof SCENE_COLORS] || SCENE_COLORS.eco; +} diff --git a/frontend/src/lib/ts/parallax/drawBase.ts b/frontend/src/lib/ts/parallax/drawBase.ts new file mode 100644 index 0000000..4eeade7 --- /dev/null +++ b/frontend/src/lib/ts/parallax/drawBase.ts @@ -0,0 +1,151 @@ +import type { DrawContext } from './types'; +import { getSceneColors } from './colors'; + +export function drawSky(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + + const gradient = ctx.createLinearGradient(0, 0, 0, height); + gradient.addColorStop(0, colors.skyTop); + gradient.addColorStop(1, colors.skyBottom); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, width, height); +} + +export function drawSun(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + + const sunX = width * 0.85 + state.mouseX * 0.3 + state.scrollY * 0.02; + const sunY = height * 0.2 + state.mouseY * 0.3 + state.scrollY * 0.05; + const sunRadius = Math.min(width, height) * 0.08; + + for (let i = 4; i >= 0; i--) { + ctx.beginPath(); + ctx.arc(sunX, sunY, sunRadius * (1 + i * 0.4), 0, Math.PI * 2); + ctx.fillStyle = colors.sunGlow; + ctx.globalAlpha = 0.2 - i * 0.03; + ctx.fill(); + } + + ctx.globalAlpha = 1; + + ctx.beginPath(); + ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2); + ctx.fillStyle = "#FDB813"; + ctx.fill(); +} + +export function drawClouds(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + + const drawCloud = (x: number, y: number, scale: number) => { + ctx.fillStyle = colors.cloud; + ctx.beginPath(); + ctx.ellipse(x, y, 60 * scale, 35 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 50 * scale, y + 10 * scale, 50 * scale, 30 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x - 40 * scale, y + 5 * scale, 45 * scale, 25 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 20 * scale, y - 15 * scale, 40 * scale, 25 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + }; + + const cloudOffsetX = state.mouseX * 0.5 - state.scrollY * 0.03; + const cloudOffsetY = state.scrollY * 0.1; + + drawCloud(width * 0.15 + cloudOffsetX, height * 0.15 + cloudOffsetY, 1.2); + drawCloud(width * 0.55 + cloudOffsetX * 0.7, height * 0.12 + cloudOffsetY * 0.8, 0.9); + drawCloud(width * 0.85 + cloudOffsetX * 0.5, height * 0.2 + cloudOffsetY * 0.6, 1.0); +} + +export function drawMountains(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + const parallaxFar = state.mouseX * 0.2 + state.scrollY * 0.15; + const parallaxMid = state.mouseX * 0.35 + state.scrollY * 0.25; + + ctx.fillStyle = colors.mountainFar; + ctx.beginPath(); + ctx.moveTo(-100 + parallaxFar, height); + ctx.lineTo(width * 0.2 + parallaxFar, height * 0.45); + ctx.lineTo(width * 0.35 + parallaxFar, height * 0.55); + ctx.lineTo(width * 0.5 + parallaxFar, height * 0.4); + ctx.lineTo(width * 0.7 + parallaxFar, height * 0.5); + ctx.lineTo(width * 0.85 + parallaxFar, height * 0.35); + ctx.lineTo(width + 100 + parallaxFar, height * 0.5); + ctx.lineTo(width + 100, height); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = colors.mountainMid; + ctx.beginPath(); + ctx.moveTo(-100 + parallaxMid, height); + ctx.lineTo(width * 0.1 + parallaxMid, height * 0.55); + ctx.lineTo(width * 0.25 + parallaxMid, height * 0.62); + ctx.lineTo(width * 0.4 + parallaxMid, height * 0.5); + ctx.lineTo(width * 0.6 + parallaxMid, height * 0.58); + ctx.lineTo(width * 0.75 + parallaxMid, height * 0.48); + ctx.lineTo(width * 0.9 + parallaxMid, height * 0.55); + ctx.lineTo(width + 100, height); + ctx.closePath(); + ctx.fill(); +} + +export function drawHills(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + const parallax = state.mouseX * 0.5 + state.scrollY * 0.35; + + ctx.fillStyle = colors.hillFront; + ctx.beginPath(); + ctx.moveTo(-100, height); + + for (let x = -100; x <= width + 100; x += 100) { + const y = height * 0.65 + Math.sin((x + parallax) * 0.01) * 40; + ctx.lineTo(x, y); + } + + ctx.lineTo(width + 100, height); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = colors.ground; + ctx.fillRect(-100, height * 0.8, width + 200, height * 0.2); +} + +export function drawWater(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + const time = Date.now() * 0.001; + const waveOffset = state.scrollY * 0.1; + + ctx.fillStyle = colors.water; + ctx.beginPath(); + ctx.moveTo(-100, height); + + for (let x = -100; x <= width + 100; x += 20) { + const y = height * 0.88 + Math.sin((x + time * 50 + waveOffset) * 0.02) * 5; + ctx.lineTo(x, y); + } + + ctx.lineTo(width + 100, height); + ctx.closePath(); + ctx.fill(); + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.lineWidth = 2; + for (let i = 0; i < 5; i++) { + ctx.beginPath(); + const startX = (width / 5) * i + Math.sin(time + i) * 20; + ctx.moveTo(startX, height * 0.9 + i * 5); + ctx.lineTo(startX + 40, height * 0.9 + i * 5); + ctx.stroke(); + } +} diff --git a/frontend/src/lib/ts/parallax/index.ts b/frontend/src/lib/ts/parallax/index.ts new file mode 100644 index 0000000..01dd85f --- /dev/null +++ b/frontend/src/lib/ts/parallax/index.ts @@ -0,0 +1,95 @@ +import type { ParallaxState, DrawContext, SceneType } from './types'; +import { drawSky, drawSun, drawClouds, drawMountains, drawHills, drawWater } from './drawBase'; +import { drawEcoScene } from './scenes/eco'; +import { drawForestScene } from './scenes/forest'; +import { drawIndustrialScene } from './scenes/industrial'; +import { drawDeforestationScene } from './scenes/deforestation'; +import { drawOceanScene, drawOceanWaves } from './scenes/ocean'; +import { drawOilRigScene } from './scenes/oilRig'; +import { drawCityScene } from './scenes/city'; +import { drawPollutedCityScene } from './scenes/pollutedCity'; + +export type { ParallaxState, SceneType }; + +const SCENE_ELEMENTS: Record, (dc: DrawContext) => void> = { + eco: drawEcoScene, + forest: drawForestScene, + industrial: drawIndustrialScene, + deforestation: drawDeforestationScene, + ocean: drawOceanScene, + oilRig: drawOilRigScene, + city: drawCityScene, + pollutedCity: drawPollutedCityScene, +}; + +const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig']; +const NO_TERRAIN_SCENES: SceneType[] = ['ocean', 'oilRig']; + +export function drawLandscape( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + state: ParallaxState +): void { + const dc: DrawContext = { ctx, width, height, state }; + const { sceneType, blendToScene, blendProgress } = state; + + ctx.clearRect(0, 0, width, height); + + drawSky(dc); + drawSun(dc); + drawClouds(dc); + + const skipTerrain = NO_TERRAIN_SCENES.includes(sceneType) || + (blendToScene && NO_TERRAIN_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5); + + if (!skipTerrain) { + drawMountains(dc); + drawHills(dc); + } + + if (sceneType === 'transition') { + const ecoOpacity = 1 - state.progress; + const industrialOpacity = state.progress; + + if (ecoOpacity > 0.1) { + ctx.globalAlpha = ecoOpacity; + drawEcoScene(dc); + } + if (industrialOpacity > 0.1) { + ctx.globalAlpha = industrialOpacity; + drawIndustrialScene(dc); + } + ctx.globalAlpha = 1; + } else if (blendToScene && blendProgress !== undefined && blendToScene !== 'transition') { + const fromOpacity = 1 - blendProgress; + const toOpacity = blendProgress; + + const fromDrawer = SCENE_ELEMENTS[sceneType as keyof typeof SCENE_ELEMENTS]; + const toDrawer = SCENE_ELEMENTS[blendToScene as keyof typeof SCENE_ELEMENTS]; + + if (fromDrawer && fromOpacity > 0.1) { + ctx.globalAlpha = fromOpacity; + fromDrawer(dc); + } + if (toDrawer && toOpacity > 0.1) { + ctx.globalAlpha = toOpacity; + toDrawer(dc); + } + ctx.globalAlpha = 1; + } else { + const drawer = SCENE_ELEMENTS[sceneType as keyof typeof SCENE_ELEMENTS]; + if (drawer) { + drawer(dc); + } + } + + const useCustomWater = CUSTOM_WATER_SCENES.includes(sceneType) || + (blendToScene && CUSTOM_WATER_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5); + + if (sceneType === 'ocean' || (blendToScene === 'ocean' && (blendProgress ?? 0) > 0.5)) { + drawOceanWaves(dc); + } else if (!useCustomWater) { + drawWater(dc); + } +} diff --git a/frontend/src/lib/ts/parallax/scenes/city.ts b/frontend/src/lib/ts/parallax/scenes/city.ts new file mode 100644 index 0000000..8015e44 --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/city.ts @@ -0,0 +1,189 @@ +import type { DrawContext } from '../types'; + +export function drawCityBuildings(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.35; + + const drawBuilding = (x: number, bWidth: number, bHeight: number, color: string, hasSpire: boolean = false) => { + const baseY = height * 0.8; + + ctx.fillStyle = color; + ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight); + + const gradient = ctx.createLinearGradient(x + parallax, 0, x + parallax + bWidth, 0); + gradient.addColorStop(0, 'rgba(255, 255, 255, 0.1)'); + gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 0.05)'); + ctx.fillStyle = gradient; + ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight); + + ctx.fillStyle = 'rgba(255, 255, 200, 0.8)'; + const windowCols = Math.floor(bWidth / 18); + const windowRows = Math.floor(bHeight / 25); + + for (let row = 0; row < windowRows; row++) { + for (let col = 0; col < windowCols; col++) { + if (Math.random() > 0.15) { + ctx.fillStyle = Math.random() > 0.7 ? 'rgba(255, 255, 200, 0.9)' : 'rgba(200, 220, 255, 0.6)'; + ctx.fillRect( + x + parallax + col * 18 + 5, + baseY - bHeight + row * 25 + 8, + 10, 12 + ); + } + } + } + + if (hasSpire) { + ctx.fillStyle = '#90A4AE'; + ctx.beginPath(); + ctx.moveTo(x + parallax + bWidth / 2, baseY - bHeight - 40); + ctx.lineTo(x + parallax + bWidth / 2 - 8, baseY - bHeight); + ctx.lineTo(x + parallax + bWidth / 2 + 8, baseY - bHeight); + ctx.closePath(); + ctx.fill(); + } + }; + + drawBuilding(width * 0.02, 55, height * 0.28, '#607D8B'); + drawBuilding(width * 0.08, 70, height * 0.42, '#78909C', true); + drawBuilding(width * 0.18, 50, height * 0.32, '#546E7A'); + drawBuilding(width * 0.25, 85, height * 0.55, '#455A64', true); + drawBuilding(width * 0.38, 60, height * 0.38, '#607D8B'); + drawBuilding(width * 0.48, 75, height * 0.48, '#78909C', true); + drawBuilding(width * 0.58, 55, height * 0.35, '#546E7A'); + drawBuilding(width * 0.68, 90, height * 0.52, '#455A64', true); + drawBuilding(width * 0.8, 65, height * 0.4, '#607D8B'); + drawBuilding(width * 0.9, 50, height * 0.3, '#78909C'); +} + +export function drawStreet(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.001; + + ctx.fillStyle = '#424242'; + ctx.fillRect(0, height * 0.82, width, height * 0.08); + + ctx.strokeStyle = '#FFEB3B'; + ctx.lineWidth = 3; + ctx.setLineDash([30, 20]); + ctx.beginPath(); + ctx.moveTo(0, height * 0.86); + ctx.lineTo(width, height * 0.86); + ctx.stroke(); + ctx.setLineDash([]); + + const drawCar = (baseX: number, y: number, color: string, direction: number) => { + const x = ((baseX + time * 50 * direction) % (width + 100)) - 50; + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.roundRect(x, y, 40, 15, 3); + ctx.fill(); + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.roundRect(x + 8, y - 10, 24, 12, 3); + ctx.fill(); + + ctx.fillStyle = '#81D4FA'; + ctx.fillRect(x + 10, y - 8, 9, 8); + ctx.fillRect(x + 21, y - 8, 9, 8); + + ctx.fillStyle = '#212121'; + ctx.beginPath(); + ctx.arc(x + 10, y + 15, 5, 0, Math.PI * 2); + ctx.arc(x + 30, y + 15, 5, 0, Math.PI * 2); + ctx.fill(); + }; + + drawCar(width * 0.1, height * 0.83, '#E53935', 1); + drawCar(width * 0.5, height * 0.83, '#1E88E5', 1); + drawCar(width * 0.3, height * 0.87, '#43A047', -1); + drawCar(width * 0.8, height * 0.87, '#FDD835', -1); +} + +export function drawAirplane(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.0003; + const parallax = state.mouseX * 0.1; + + const x = (time * width) % (width + 200) - 100; + const y = height * 0.15 + Math.sin(time * 5) * 10; + + ctx.save(); + ctx.translate(x + parallax, y); + + ctx.fillStyle = '#ECEFF1'; + ctx.beginPath(); + ctx.ellipse(0, 0, 40, 8, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.beginPath(); + ctx.ellipse(45, 0, 12, 6, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#B0BEC5'; + ctx.beginPath(); + ctx.moveTo(-10, 0); + ctx.lineTo(-30, -35); + ctx.lineTo(10, -35); + ctx.lineTo(15, 0); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(-10, 0); + ctx.lineTo(-30, 35); + ctx.lineTo(10, 35); + ctx.lineTo(15, 0); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = '#E53935'; + ctx.beginPath(); + ctx.moveTo(-35, 0); + ctx.lineTo(-50, -20); + ctx.lineTo(-30, 0); + ctx.closePath(); + ctx.fill(); + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(-50, 0); + ctx.lineTo(-150, 3); + ctx.stroke(); + + ctx.restore(); +} + +export function drawParks(dc: DrawContext): void { + const { ctx, width, height } = dc; + + ctx.fillStyle = '#81C784'; + + const parks = [ + { x: width * 0.05, y: height * 0.81, rx: 25, ry: 8 }, + { x: width * 0.35, y: height * 0.81, rx: 30, ry: 10 }, + { x: width * 0.75, y: height * 0.81, rx: 35, ry: 9 }, + ]; + + parks.forEach(p => { + ctx.beginPath(); + ctx.ellipse(p.x, p.y, p.rx, p.ry, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#4CAF50'; + ctx.beginPath(); + ctx.arc(p.x - 10, p.y - 5, 8, 0, Math.PI * 2); + ctx.arc(p.x + 10, p.y - 3, 6, 0, Math.PI * 2); + ctx.fill(); + }); +} + +export function drawCityScene(dc: DrawContext): void { + drawAirplane(dc); + drawCityBuildings(dc); + drawParks(dc); + drawStreet(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/deforestation.ts b/frontend/src/lib/ts/parallax/scenes/deforestation.ts new file mode 100644 index 0000000..f599b20 --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/deforestation.ts @@ -0,0 +1,137 @@ +import type { DrawContext } from '../types'; + +export function drawStumps(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.7 + state.scrollY * 0.5; + + const drawStump = (x: number, y: number, scale: number) => { + ctx.fillStyle = '#5D4037'; + ctx.fillRect(x - 12 * scale + parallax, y - 8 * scale, 24 * scale, 25 * scale); + + ctx.fillStyle = '#8D6E63'; + ctx.beginPath(); + ctx.ellipse(x + parallax, y - 8 * scale, 15 * scale, 8 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#5D4037'; + ctx.lineWidth = 1; + for (let ring = 1; ring <= 3; ring++) { + ctx.beginPath(); + ctx.ellipse(x + parallax, y - 8 * scale, ring * 4 * scale, ring * 2.5 * scale, 0, 0, Math.PI * 2); + ctx.stroke(); + } + }; + + const stumpPositions = [ + { x: width * 0.08, y: height * 0.78, scale: 1.2 }, + { x: width * 0.18, y: height * 0.82, scale: 0.9 }, + { x: width * 0.28, y: height * 0.76, scale: 1.4 }, + { x: width * 0.42, y: height * 0.8, scale: 1.0 }, + { x: width * 0.55, y: height * 0.78, scale: 1.3 }, + { x: width * 0.68, y: height * 0.82, scale: 0.8 }, + { x: width * 0.78, y: height * 0.77, scale: 1.5 }, + { x: width * 0.9, y: height * 0.8, scale: 1.1 }, + ]; + + stumpPositions.forEach(s => drawStump(s.x, s.y, s.scale)); +} + +export function drawFallenLogs(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.6; + + const drawLog = (x: number, y: number, length: number, angle: number) => { + ctx.save(); + ctx.translate(x + parallax, y); + ctx.rotate(angle); + + ctx.fillStyle = '#6D4C41'; + ctx.beginPath(); + ctx.ellipse(0, 0, length, 12, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#8D6E63'; + ctx.beginPath(); + ctx.ellipse(length - 5, 0, 8, 12, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#4E342E'; + ctx.lineWidth = 2; + for (let i = -length + 20; i < length - 20; i += 25) { + ctx.beginPath(); + ctx.moveTo(i, -10); + ctx.lineTo(i + 10, 10); + ctx.stroke(); + } + + ctx.restore(); + }; + + drawLog(width * 0.2, height * 0.85, 80, 0.1); + drawLog(width * 0.5, height * 0.86, 100, -0.15); + drawLog(width * 0.75, height * 0.84, 70, 0.2); +} + +export function drawLoggingTruck(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.4; + const x = width * 0.65 + parallax; + const y = height * 0.75; + + ctx.fillStyle = '#FF9800'; + ctx.fillRect(x, y, 50, 35); + ctx.fillStyle = '#F57C00'; + ctx.fillRect(x, y, 50, 8); + + ctx.fillStyle = '#81D4FA'; + ctx.fillRect(x + 5, y + 10, 40, 15); + + ctx.fillStyle = '#616161'; + ctx.fillRect(x + 55, y + 5, 90, 30); + + ctx.fillStyle = '#8D6E63'; + for (let i = 0; i < 3; i++) { + ctx.beginPath(); + ctx.ellipse(x + 75 + i * 25, y + 5, 12, 20, Math.PI / 2, 0, Math.PI * 2); + ctx.fill(); + } + + ctx.fillStyle = '#212121'; + ctx.beginPath(); + ctx.arc(x + 15, y + 38, 12, 0, Math.PI * 2); + ctx.arc(x + 80, y + 38, 12, 0, Math.PI * 2); + ctx.arc(x + 120, y + 38, 12, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#757575'; + ctx.beginPath(); + ctx.arc(x + 15, y + 38, 6, 0, Math.PI * 2); + ctx.arc(x + 80, y + 38, 6, 0, Math.PI * 2); + ctx.arc(x + 120, y + 38, 6, 0, Math.PI * 2); + ctx.fill(); +} + +export function drawDirtPatches(dc: DrawContext): void { + const { ctx, width, height } = dc; + + ctx.fillStyle = '#4E342E'; + + const patches = [ + { x: width * 0.15, y: height * 0.83, rx: 40, ry: 15 }, + { x: width * 0.45, y: height * 0.85, rx: 60, ry: 20 }, + { x: width * 0.7, y: height * 0.82, rx: 50, ry: 18 }, + ]; + + patches.forEach(p => { + ctx.beginPath(); + ctx.ellipse(p.x, p.y, p.rx, p.ry, 0, 0, Math.PI * 2); + ctx.fill(); + }); +} + +export function drawDeforestationScene(dc: DrawContext): void { + drawDirtPatches(dc); + drawStumps(dc); + drawFallenLogs(dc); + drawLoggingTruck(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/eco.ts b/frontend/src/lib/ts/parallax/scenes/eco.ts new file mode 100644 index 0000000..5aff88d --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/eco.ts @@ -0,0 +1,145 @@ +import type { DrawContext } from '../types'; +import { getSceneColors } from '../colors'; + +const FLOWER_COLORS = ['#E91E63', '#FF5722', '#FFEB3B', '#9C27B0', '#FF4081']; + +export function drawEcoTrees(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + const parallax = state.mouseX * 0.8 + state.scrollY * 0.5; + + const drawTree = (x: number, y: number, scale: number) => { + ctx.fillStyle = '#5D4037'; + ctx.fillRect(x - 8 * scale + parallax, y, 16 * scale, 40 * scale); + + ctx.fillStyle = colors.treeDark; + ctx.beginPath(); + ctx.moveTo(x + parallax, y - 80 * scale); + ctx.lineTo(x - 35 * scale + parallax, y); + ctx.lineTo(x + 35 * scale + parallax, y); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = colors.treeLight; + ctx.beginPath(); + ctx.moveTo(x + parallax, y - 100 * scale); + ctx.lineTo(x - 25 * scale + parallax, y - 30 * scale); + ctx.lineTo(x + 25 * scale + parallax, y - 30 * scale); + ctx.closePath(); + ctx.fill(); + }; + + const treePositions = [ + { x: width * 0.1, y: height * 0.75, scale: 1.3 }, + { x: width * 0.2, y: height * 0.78, scale: 1.0 }, + { x: width * 0.35, y: height * 0.73, scale: 1.5 }, + { x: width * 0.65, y: height * 0.76, scale: 1.2 }, + { x: width * 0.8, y: height * 0.74, scale: 1.4 }, + { x: width * 0.92, y: height * 0.77, scale: 1.1 }, + ]; + + treePositions.forEach((t) => drawTree(t.x, t.y, t.scale)); +} + +export function drawFlowers(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 1.0 + state.scrollY * 0.6; + const time = Date.now() * 0.001; + + const drawFlower = (x: number, y: number, color: string, size: number) => { + const sway = Math.sin(time + x * 0.01) * 3; + ctx.strokeStyle = '#2E7D32'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x + parallax, y + 15); + ctx.quadraticCurveTo(x + parallax + sway, y + 7, x + parallax, y); + ctx.stroke(); + + ctx.fillStyle = color; + for (let i = 0; i < 5; i++) { + ctx.beginPath(); + const angle = (i * Math.PI * 2) / 5; + ctx.ellipse( + x + Math.cos(angle) * size * 0.5 + parallax, + y + Math.sin(angle) * size * 0.5, + size * 0.4, size * 0.25, angle, 0, Math.PI * 2 + ); + ctx.fill(); + } + + ctx.fillStyle = '#FFEB3B'; + ctx.beginPath(); + ctx.arc(x + parallax, y, size * 0.3, 0, Math.PI * 2); + ctx.fill(); + }; + + for (let i = 0; i < 25; i++) { + const x = (i * width) / 25 + (i % 3) * 15; + const y = height * 0.82 + Math.sin(i * 1.5) * 12; + const color = FLOWER_COLORS[i % FLOWER_COLORS.length]; + drawFlower(x, y, color, 6 + (i % 4) * 2); + } +} + +export function drawBirds(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + ctx.strokeStyle = '#37474F'; + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + + const parallax = state.mouseX * 0.15 - state.scrollY * 0.02; + const time = Date.now() * 0.002; + + const drawBird = (x: number, y: number, size: number, phase: number) => { + const wingOffset = Math.sin(time + phase) * 5; + ctx.beginPath(); + ctx.moveTo(x + parallax - 10 * size, y + wingOffset); + ctx.quadraticCurveTo(x + parallax, y - 5 * size, x + parallax + 10 * size, y + wingOffset); + ctx.stroke(); + }; + + drawBird(width * 0.3, height * 0.25, 0.8, 0); + drawBird(width * 0.35, height * 0.28, 0.6, 1); + drawBird(width * 0.45, height * 0.22, 0.7, 2); + drawBird(width * 0.7, height * 0.18, 0.9, 0.5); + drawBird(width * 0.75, height * 0.21, 0.5, 1.5); +} + +export function drawButterflies(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.003; + const parallax = state.mouseX * 0.3; + + const drawButterfly = (x: number, y: number, color: string, phase: number) => { + const wingFlap = Math.sin(time * 5 + phase) * 0.3 + 0.7; + const floatY = Math.sin(time + phase) * 10; + const floatX = Math.cos(time * 0.5 + phase) * 15; + + ctx.save(); + ctx.translate(x + parallax + floatX, y + floatY); + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.ellipse(-8 * wingFlap, 0, 8, 12, -0.3, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(8 * wingFlap, 0, 8, 12, 0.3, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(-1, -8, 2, 16); + + ctx.restore(); + }; + + drawButterfly(width * 0.25, height * 0.6, '#E91E63', 0); + drawButterfly(width * 0.55, height * 0.55, '#FFEB3B', 1.5); + drawButterfly(width * 0.75, height * 0.62, '#9C27B0', 3); +} + +export function drawEcoScene(dc: DrawContext): void { + drawEcoTrees(dc); + drawFlowers(dc); + drawBirds(dc); + drawButterflies(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/forest.ts b/frontend/src/lib/ts/parallax/scenes/forest.ts new file mode 100644 index 0000000..a76acc6 --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/forest.ts @@ -0,0 +1,168 @@ +import type { DrawContext } from '../types'; +import { getSceneColors } from '../colors'; + +export function drawForestTrees(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + const parallax = state.mouseX * 0.8 + state.scrollY * 0.5; + + const drawPineTree = (x: number, y: number, scale: number, dark: boolean) => { + ctx.fillStyle = dark ? '#3E2723' : '#5D4037'; + ctx.fillRect(x - 6 * scale + parallax, y, 12 * scale, 35 * scale); + + const foliageColor = dark ? colors.treeDark : colors.treeLight; + ctx.fillStyle = foliageColor; + + for (let layer = 0; layer < 4; layer++) { + const layerY = y + 5 * scale - layer * 22 * scale; + const layerWidth = (35 - layer * 5) * scale; + ctx.beginPath(); + ctx.moveTo(x + parallax, layerY - 25 * scale); + ctx.lineTo(x - layerWidth + parallax, layerY); + ctx.lineTo(x + layerWidth + parallax, layerY); + ctx.closePath(); + ctx.fill(); + } + }; + + const treePositions = [ + { x: width * 0.05, y: height * 0.68, scale: 0.7, dark: true }, + { x: width * 0.15, y: height * 0.66, scale: 0.8, dark: true }, + { x: width * 0.28, y: height * 0.67, scale: 0.6, dark: true }, + { x: width * 0.42, y: height * 0.65, scale: 0.75, dark: true }, + { x: width * 0.58, y: height * 0.68, scale: 0.65, dark: true }, + { x: width * 0.72, y: height * 0.66, scale: 0.7, dark: true }, + { x: width * 0.88, y: height * 0.67, scale: 0.8, dark: true }, + { x: width * 0.95, y: height * 0.68, scale: 0.6, dark: true }, + { x: width * 0.08, y: height * 0.76, scale: 1.4, dark: false }, + { x: width * 0.22, y: height * 0.78, scale: 1.2, dark: false }, + { x: width * 0.38, y: height * 0.74, scale: 1.6, dark: false }, + { x: width * 0.52, y: height * 0.77, scale: 1.3, dark: false }, + { x: width * 0.68, y: height * 0.75, scale: 1.5, dark: false }, + { x: width * 0.82, y: height * 0.78, scale: 1.1, dark: false }, + { x: width * 0.94, y: height * 0.76, scale: 1.4, dark: false }, + ]; + + treePositions.forEach((t) => drawPineTree(t.x, t.y, t.scale, t.dark)); +} + +export function drawMushrooms(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.9 + state.scrollY * 0.6; + + const drawMushroom = (x: number, y: number, scale: number, isRed: boolean) => { + ctx.fillStyle = '#F5F5DC'; + ctx.beginPath(); + ctx.ellipse(x + parallax, y + 8 * scale, 6 * scale, 10 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = isRed ? '#D32F2F' : '#8D6E63'; + ctx.beginPath(); + ctx.ellipse(x + parallax, y - 2 * scale, 12 * scale, 8 * scale, 0, Math.PI, Math.PI * 2); + ctx.fill(); + + if (isRed) { + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + ctx.arc(x + parallax - 4, y - 4 * scale, 2 * scale, 0, Math.PI * 2); + ctx.arc(x + parallax + 5, y - 3 * scale, 1.5 * scale, 0, Math.PI * 2); + ctx.fill(); + } + }; + + drawMushroom(width * 0.15, height * 0.84, 1.0, true); + drawMushroom(width * 0.35, height * 0.86, 0.8, false); + drawMushroom(width * 0.52, height * 0.83, 1.2, true); + drawMushroom(width * 0.78, height * 0.85, 0.9, false); + drawMushroom(width * 0.88, height * 0.84, 1.1, true); +} + +export function drawDeer(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.001; + const parallax = state.mouseX * 0.5; + + const x = width * 0.6 + parallax; + const y = height * 0.72; + const headBob = Math.sin(time * 2) * 3; + + ctx.fillStyle = '#8D6E63'; + ctx.beginPath(); + ctx.ellipse(x, y, 35, 25, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#6D4C41'; + ctx.fillRect(x - 20, y + 15, 8, 30); + ctx.fillRect(x - 5, y + 18, 8, 28); + ctx.fillRect(x + 10, y + 15, 8, 30); + ctx.fillRect(x + 25, y + 18, 8, 28); + + ctx.fillStyle = '#8D6E63'; + ctx.beginPath(); + ctx.ellipse(x + 40, y - 15 + headBob, 12, 18, 0.3, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + 55, y - 25 + headBob, 10, 12, 0.2, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#5D4037'; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(x + 50, y - 35 + headBob); + ctx.lineTo(x + 45, y - 50 + headBob); + ctx.lineTo(x + 40, y - 45 + headBob); + ctx.moveTo(x + 45, y - 50 + headBob); + ctx.lineTo(x + 50, y - 55 + headBob); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(x + 60, y - 35 + headBob); + ctx.lineTo(x + 65, y - 50 + headBob); + ctx.lineTo(x + 70, y - 45 + headBob); + ctx.moveTo(x + 65, y - 50 + headBob); + ctx.lineTo(x + 60, y - 55 + headBob); + ctx.stroke(); + + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.arc(x + 60, y - 25 + headBob, 3, 0, Math.PI * 2); + ctx.fill(); +} + +export function drawLightRays(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.0005; + + ctx.save(); + ctx.globalAlpha = 0.15 + Math.sin(time) * 0.05; + + const gradient = ctx.createLinearGradient(width * 0.7, 0, width * 0.5, height * 0.7); + gradient.addColorStop(0, 'rgba(255, 255, 200, 0.8)'); + gradient.addColorStop(1, 'rgba(255, 255, 200, 0)'); + + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.moveTo(width * 0.75, 0); + ctx.lineTo(width * 0.85, 0); + ctx.lineTo(width * 0.55, height * 0.7); + ctx.lineTo(width * 0.45, height * 0.7); + ctx.closePath(); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(width * 0.55, 0); + ctx.lineTo(width * 0.62, 0); + ctx.lineTo(width * 0.35, height * 0.65); + ctx.lineTo(width * 0.28, height * 0.65); + ctx.closePath(); + ctx.fill(); + + ctx.restore(); +} + +export function drawForestScene(dc: DrawContext): void { + drawLightRays(dc); + drawForestTrees(dc); + drawMushrooms(dc); + drawDeer(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/industrial.ts b/frontend/src/lib/ts/parallax/scenes/industrial.ts new file mode 100644 index 0000000..eda3571 --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/industrial.ts @@ -0,0 +1,130 @@ +import type { DrawContext } from '../types'; + +export function drawFactories(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.4 + state.scrollY * 0.3; + + const drawFactory = (x: number, baseY: number, factoryWidth: number, factoryHeight: number) => { + ctx.fillStyle = '#424242'; + ctx.fillRect(x + parallax, baseY - factoryHeight, factoryWidth, factoryHeight); + + ctx.fillStyle = '#2E2E2E'; + ctx.fillRect(x + parallax, baseY - factoryHeight, factoryWidth * 0.1, factoryHeight); + + ctx.fillStyle = 'rgba(255, 200, 100, 0.6)'; + const windowRows = 3; + const windowCols = 4; + const windowW = factoryWidth * 0.12; + const windowH = factoryHeight * 0.08; + const spacingX = (factoryWidth - windowCols * windowW) / (windowCols + 1); + const spacingY = (factoryHeight * 0.6 - windowRows * windowH) / (windowRows + 1); + + for (let row = 0; row < windowRows; row++) { + for (let col = 0; col < windowCols; col++) { + const wx = x + parallax + spacingX * (col + 1) + windowW * col; + const wy = baseY - factoryHeight + spacingY * (row + 1) + windowH * row + factoryHeight * 0.3; + ctx.fillRect(wx, wy, windowW, windowH); + } + } + + const chimneyWidth = factoryWidth * 0.15; + const chimneyHeight = factoryHeight * 0.6; + ctx.fillStyle = '#263238'; + ctx.fillRect(x + parallax + factoryWidth * 0.7, baseY - factoryHeight - chimneyHeight, chimneyWidth, chimneyHeight); + + ctx.fillStyle = '#D32F2F'; + ctx.fillRect(x + parallax + factoryWidth * 0.7, baseY - factoryHeight - chimneyHeight, chimneyWidth, 10); + ctx.fillRect(x + parallax + factoryWidth * 0.7, baseY - factoryHeight - chimneyHeight * 0.5, chimneyWidth, 10); + + drawSmoke(ctx, x + parallax + factoryWidth * 0.7 + chimneyWidth / 2, baseY - factoryHeight - chimneyHeight); + }; + + drawFactory(width * 0.08, height * 0.8, width * 0.18, height * 0.28); + drawFactory(width * 0.35, height * 0.8, width * 0.22, height * 0.38); + drawFactory(width * 0.7, height * 0.8, width * 0.15, height * 0.3); +} + +function drawSmoke(ctx: CanvasRenderingContext2D, x: number, baseY: number): void { + const time = Date.now() * 0.001; + + ctx.fillStyle = 'rgba(100, 100, 100, 0.5)'; + for (let i = 0; i < 6; i++) { + const smokeY = baseY - i * 22 - Math.sin(time + i) * 8; + const smokeRadius = 12 + i * 10 + Math.sin(time * 2 + i) * 4; + const offsetX = Math.sin(time + i * 0.5) * 20; + const alpha = 0.5 - i * 0.07; + + ctx.globalAlpha = alpha; + ctx.beginPath(); + ctx.arc(x + offsetX, smokeY, smokeRadius, 0, Math.PI * 2); + ctx.fill(); + } + ctx.globalAlpha = 1; +} + +export function drawPowerLines(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.3; + + ctx.fillStyle = '#37474F'; + const polePositions = [width * 0.25, width * 0.6, width * 0.95]; + + polePositions.forEach(px => { + ctx.fillRect(px + parallax - 4, height * 0.5, 8, height * 0.35); + ctx.fillRect(px + parallax - 25, height * 0.52, 50, 6); + }); + + ctx.strokeStyle = '#1a1a1a'; + ctx.lineWidth = 2; + + for (let wire = 0; wire < 3; wire++) { + ctx.beginPath(); + const yOffset = wire * 8; + ctx.moveTo(polePositions[0] + parallax - 20, height * 0.52 + yOffset); + + const midX = (polePositions[0] + polePositions[1]) / 2 + parallax; + ctx.quadraticCurveTo(midX, height * 0.58 + yOffset, polePositions[1] + parallax + 20, height * 0.52 + yOffset); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(polePositions[1] + parallax - 20, height * 0.52 + yOffset); + const midX2 = (polePositions[1] + polePositions[2]) / 2 + parallax; + ctx.quadraticCurveTo(midX2, height * 0.58 + yOffset, polePositions[2] + parallax + 20, height * 0.52 + yOffset); + ctx.stroke(); + } +} + +export function drawDeadTrees(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.6; + + const drawDeadTree = (x: number, y: number, scale: number) => { + ctx.strokeStyle = '#4E342E'; + ctx.lineWidth = 8 * scale; + ctx.lineCap = 'round'; + + ctx.beginPath(); + ctx.moveTo(x + parallax, y + 40 * scale); + ctx.lineTo(x + parallax, y - 30 * scale); + ctx.stroke(); + + ctx.lineWidth = 4 * scale; + ctx.beginPath(); + ctx.moveTo(x + parallax, y - 10 * scale); + ctx.lineTo(x + parallax - 25 * scale, y - 40 * scale); + ctx.moveTo(x + parallax, y - 20 * scale); + ctx.lineTo(x + parallax + 30 * scale, y - 45 * scale); + ctx.moveTo(x + parallax, y - 30 * scale); + ctx.lineTo(x + parallax - 15 * scale, y - 55 * scale); + ctx.stroke(); + }; + + drawDeadTree(width * 0.02, height * 0.78, 1.0); + drawDeadTree(width * 0.88, height * 0.76, 1.2); +} + +export function drawIndustrialScene(dc: DrawContext): void { + drawPowerLines(dc); + drawFactories(dc); + drawDeadTrees(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/ocean.ts b/frontend/src/lib/ts/parallax/scenes/ocean.ts new file mode 100644 index 0000000..af38897 --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/ocean.ts @@ -0,0 +1,197 @@ +import type { DrawContext } from '../types'; +import { getSceneColors } from '../colors'; + +export function drawOceanWaves(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const colors = getSceneColors(state); + const time = Date.now() * 0.001; + + for (let layer = 0; layer < 3; layer++) { + const alpha = 0.3 + layer * 0.25; + const yBase = height * (0.55 + layer * 0.12); + const waveHeight = 15 - layer * 3; + const speed = 40 + layer * 20; + + ctx.fillStyle = layer === 2 ? colors.water : `rgba(0, 150, 200, ${alpha})`; + ctx.beginPath(); + ctx.moveTo(-100, height); + + for (let x = -100; x <= width + 100; x += 10) { + const y = yBase + Math.sin((x + time * speed) * 0.02) * waveHeight; + ctx.lineTo(x, y); + } + + ctx.lineTo(width + 100, height); + ctx.closePath(); + ctx.fill(); + } + + ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; + for (let i = 0; i < 8; i++) { + const x = (width / 8) * i + Math.sin(time + i * 2) * 30; + const y = height * 0.58 + Math.sin(time * 1.5 + i) * 8; + ctx.beginPath(); + ctx.ellipse(x, y, 20 + i * 3, 5, 0, 0, Math.PI * 2); + ctx.fill(); + } +} + +export function drawBoats(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.001; + const parallax = state.mouseX * 0.3; + + const drawSailboat = (x: number, baseY: number, scale: number, phase: number) => { + const bob = Math.sin(time * 2 + phase) * 5; + const y = baseY + bob; + + ctx.fillStyle = '#5D4037'; + ctx.beginPath(); + ctx.moveTo(x + parallax - 30 * scale, y); + ctx.lineTo(x + parallax + 30 * scale, y); + ctx.lineTo(x + parallax + 20 * scale, y + 15 * scale); + ctx.lineTo(x + parallax - 20 * scale, y + 15 * scale); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = '#8D6E63'; + ctx.fillRect(x + parallax - 2 * scale, y - 60 * scale, 4 * scale, 60 * scale); + + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + ctx.moveTo(x + parallax, y - 55 * scale); + ctx.lineTo(x + parallax + 25 * scale, y - 10 * scale); + ctx.lineTo(x + parallax, y - 10 * scale); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = '#E91E63'; + ctx.beginPath(); + ctx.moveTo(x + parallax, y - 60 * scale); + ctx.lineTo(x + parallax + 12 * scale, y - 55 * scale); + ctx.lineTo(x + parallax, y - 50 * scale); + ctx.closePath(); + ctx.fill(); + }; + + drawSailboat(width * 0.2, height * 0.58, 1.0, 0); + drawSailboat(width * 0.7, height * 0.62, 1.3, 1.5); + drawSailboat(width * 0.9, height * 0.56, 0.8, 3); +} + +export function drawDolphins(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.001; + + const drawDolphin = (baseX: number, baseY: number, scale: number, phase: number) => { + const cycleLength = 3; + const cycleProgress = ((time + phase) % cycleLength) / cycleLength; + + let jumpProgress = 0; + if (cycleProgress > 0.2 && cycleProgress < 0.8) { + jumpProgress = (cycleProgress - 0.2) / 0.6; + } else { + return; + } + + const jumpHeight = 80 * scale * Math.sin(jumpProgress * Math.PI); + const x = baseX + (jumpProgress - 0.5) * 150 * scale; + const y = baseY - jumpHeight; + + const rotation = Math.cos(jumpProgress * Math.PI) * -0.8; + + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation); + + ctx.fillStyle = '#546E7A'; + ctx.beginPath(); + ctx.ellipse(0, 0, 30 * scale, 12 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(-5 * scale, -10 * scale); + ctx.lineTo(5 * scale, -25 * scale); + ctx.lineTo(12 * scale, -8 * scale); + ctx.closePath(); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(-28 * scale, 0); + ctx.lineTo(-45 * scale, -12 * scale); + ctx.lineTo(-35 * scale, 0); + ctx.lineTo(-45 * scale, 12 * scale); + ctx.closePath(); + ctx.fill(); + + ctx.beginPath(); + ctx.ellipse(35 * scale, 0, 12 * scale, 6 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.arc(28 * scale, -3 * scale, 2 * scale, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + }; + + drawDolphin(width * 0.25, height * 0.75, 1.0, 0); + drawDolphin(width * 0.5, height * 0.78, 0.8, 1); +} + +export function drawSeagulls(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.003; + const parallax = state.mouseX * 0.2; + + const drawSeagull = (x: number, y: number, scale: number, phase: number) => { + const wingFlap = Math.sin(time * 3 + phase) * 0.4; + + ctx.save(); + ctx.translate(x + parallax, y); + + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + ctx.ellipse(0, 0, 10 * scale, 5 * scale, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#FFFFFF'; + ctx.lineWidth = 3 * scale; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(-12 * scale, 0); + ctx.quadraticCurveTo(-18 * scale, -15 * scale * (1 + wingFlap), -25 * scale, -5 * scale); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(12 * scale, 0); + ctx.quadraticCurveTo(18 * scale, -15 * scale * (1 + wingFlap), 25 * scale, -5 * scale); + ctx.stroke(); + + ctx.fillStyle = '#FFFFFF'; + ctx.beginPath(); + ctx.arc(12 * scale, -2 * scale, 4 * scale, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#FF9800'; + ctx.beginPath(); + ctx.moveTo(16 * scale, -2 * scale); + ctx.lineTo(22 * scale, 0); + ctx.lineTo(16 * scale, 2 * scale); + ctx.closePath(); + ctx.fill(); + + ctx.restore(); + }; + + drawSeagull(width * 0.15, height * 0.2, 1.0, 0); + drawSeagull(width * 0.35, height * 0.15, 0.8, 1); + drawSeagull(width * 0.55, height * 0.22, 1.2, 2); + drawSeagull(width * 0.8, height * 0.18, 0.9, 3); +} + +export function drawOceanScene(dc: DrawContext): void { + drawSeagulls(dc); + drawBoats(dc); + drawDolphins(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/oilRig.ts b/frontend/src/lib/ts/parallax/scenes/oilRig.ts new file mode 100644 index 0000000..61dfa19 --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/oilRig.ts @@ -0,0 +1,162 @@ +import type { DrawContext } from '../types'; + +export function drawOilRigs(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.3; + + const drawRig = (x: number, baseY: number, scale: number) => { + const time = Date.now() * 0.005; + + ctx.fillStyle = '#37474F'; + ctx.fillRect(x - 50 * scale + parallax, baseY, 10 * scale, 80 * scale); + ctx.fillRect(x + 40 * scale + parallax, baseY, 10 * scale, 80 * scale); + + ctx.strokeStyle = '#455A64'; + ctx.lineWidth = 4 * scale; + ctx.beginPath(); + ctx.moveTo(x - 45 * scale + parallax, baseY + 20 * scale); + ctx.lineTo(x + 45 * scale + parallax, baseY + 60 * scale); + ctx.moveTo(x + 45 * scale + parallax, baseY + 20 * scale); + ctx.lineTo(x - 45 * scale + parallax, baseY + 60 * scale); + ctx.stroke(); + + ctx.fillStyle = '#546E7A'; + ctx.fillRect(x - 70 * scale + parallax, baseY - 25 * scale, 140 * scale, 25 * scale); + + ctx.fillStyle = '#455A64'; + ctx.fillRect(x - 40 * scale + parallax, baseY - 60 * scale, 40 * scale, 35 * scale); + + ctx.fillStyle = 'rgba(255, 200, 100, 0.7)'; + ctx.fillRect(x - 35 * scale + parallax, baseY - 50 * scale, 12 * scale, 10 * scale); + ctx.fillRect(x - 18 * scale + parallax, baseY - 50 * scale, 12 * scale, 10 * scale); + + ctx.fillStyle = '#37474F'; + ctx.beginPath(); + ctx.moveTo(x + 20 * scale + parallax, baseY - 25 * scale); + ctx.lineTo(x + 10 * scale + parallax, baseY - 120 * scale); + ctx.lineTo(x + 50 * scale + parallax, baseY - 120 * scale); + ctx.lineTo(x + 40 * scale + parallax, baseY - 25 * scale); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = '#FF5722'; + ctx.fillRect(x + 20 * scale + parallax, baseY - 100 * scale, 70 * scale, 6 * scale); + + const flameX = x + 90 * scale + parallax; + const flameY = baseY - 100 * scale; + + ctx.fillStyle = '#263238'; + ctx.fillRect(x + 85 * scale + parallax, baseY - 25 * scale, 10 * scale, -75 * scale); + + ctx.fillStyle = '#FF6F00'; + ctx.beginPath(); + ctx.moveTo(flameX - 5 * scale, flameY); + ctx.quadraticCurveTo(flameX + Math.sin(time) * 8, flameY - 35 * scale, flameX + 5 * scale, flameY); + ctx.fill(); + + ctx.fillStyle = '#FFEB3B'; + ctx.beginPath(); + ctx.moveTo(flameX - 3 * scale, flameY); + ctx.quadraticCurveTo(flameX + Math.sin(time * 1.5) * 5, flameY - 25 * scale, flameX + 3 * scale, flameY); + ctx.fill(); + }; + + drawRig(width * 0.25, height * 0.6, 1.0); + drawRig(width * 0.75, height * 0.55, 1.3); +} + +export function drawOilTanker(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const time = Date.now() * 0.001; + const parallax = state.mouseX * 0.4; + + const x = width * 0.5 + parallax; + const y = height * 0.72 + Math.sin(time) * 3; + + ctx.fillStyle = '#1C313A'; + ctx.beginPath(); + ctx.moveTo(x - 100, y); + ctx.lineTo(x + 100, y); + ctx.lineTo(x + 80, y + 25); + ctx.lineTo(x - 80, y + 25); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = '#B71C1C'; + ctx.beginPath(); + ctx.moveTo(x - 90, y + 15); + ctx.lineTo(x + 90, y + 15); + ctx.lineTo(x + 80, y + 25); + ctx.lineTo(x - 80, y + 25); + ctx.closePath(); + ctx.fill(); + + ctx.fillStyle = '#37474F'; + ctx.fillRect(x - 95, y - 8, 190, 8); + + ctx.fillStyle = '#455A64'; + for (let i = 0; i < 5; i++) { + ctx.beginPath(); + ctx.ellipse(x - 70 + i * 35, y - 15, 15, 20, 0, Math.PI, 0); + ctx.fill(); + } + + ctx.fillStyle = '#263238'; + ctx.fillRect(x + 60, y - 35, 30, 30); + ctx.fillStyle = 'rgba(255, 200, 100, 0.6)'; + ctx.fillRect(x + 65, y - 30, 8, 8); + ctx.fillRect(x + 78, y - 30, 8, 8); + + ctx.fillStyle = '#37474F'; + ctx.fillRect(x + 70, y - 50, 10, 20); + + ctx.fillStyle = 'rgba(60, 60, 60, 0.4)'; + for (let i = 0; i < 3; i++) { + ctx.beginPath(); + ctx.arc(x + 75 + Math.sin(time + i) * 8, y - 55 - i * 15, 8 + i * 5, 0, Math.PI * 2); + ctx.fill(); + } +} + +export function drawOilSlicks(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.0005; + + ctx.fillStyle = '#1C313A'; + ctx.fillRect(0, height * 0.75, width, height * 0.25); + + const slicks = [ + { x: width * 0.15, y: height * 0.82, rx: 80, ry: 25 }, + { x: width * 0.45, y: height * 0.85, rx: 120, ry: 30 }, + { x: width * 0.75, y: height * 0.8, rx: 90, ry: 28 }, + ]; + + slicks.forEach((slick, i) => { + ctx.fillStyle = 'rgba(20, 20, 20, 0.7)'; + ctx.beginPath(); + ctx.ellipse(slick.x, slick.y, slick.rx, slick.ry, 0, 0, Math.PI * 2); + ctx.fill(); + + const gradient = ctx.createRadialGradient( + slick.x - slick.rx * 0.3, slick.y, + 0, + slick.x, slick.y, + slick.rx + ); + gradient.addColorStop(0, 'rgba(128, 0, 128, 0.3)'); + gradient.addColorStop(0.3, 'rgba(0, 100, 200, 0.2)'); + gradient.addColorStop(0.6, 'rgba(0, 200, 100, 0.15)'); + gradient.addColorStop(1, 'rgba(200, 200, 0, 0.1)'); + + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.ellipse(slick.x + Math.sin(time + i) * 5, slick.y, slick.rx * 0.9, slick.ry * 0.9, 0, 0, Math.PI * 2); + ctx.fill(); + }); +} + +export function drawOilRigScene(dc: DrawContext): void { + drawOilSlicks(dc); + drawOilRigs(dc); + drawOilTanker(dc); +} diff --git a/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts b/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts new file mode 100644 index 0000000..7c35a4d --- /dev/null +++ b/frontend/src/lib/ts/parallax/scenes/pollutedCity.ts @@ -0,0 +1,176 @@ +import type { DrawContext } from '../types'; + +export function drawSmoggyBuildings(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.35; + + const drawBuilding = (x: number, bWidth: number, bHeight: number) => { + const baseY = height * 0.8; + + ctx.fillStyle = '#37474F'; + ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + ctx.fillRect(x + parallax, baseY - bHeight * 0.8, bWidth * 0.5, bHeight * 0.3); + ctx.fillRect(x + parallax + bWidth * 0.6, baseY - bHeight * 0.5, bWidth * 0.3, bHeight * 0.2); + + const windowCols = Math.floor(bWidth / 18); + const windowRows = Math.floor(bHeight / 25); + + for (let row = 0; row < windowRows; row++) { + for (let col = 0; col < windowCols; col++) { + const isBroken = Math.random() > 0.85; + const isLit = Math.random() > 0.5; + + if (!isBroken) { + ctx.fillStyle = isLit ? 'rgba(255, 180, 100, 0.5)' : 'rgba(50, 50, 50, 0.8)'; + ctx.fillRect( + x + parallax + col * 18 + 5, + baseY - bHeight + row * 25 + 8, + 10, 12 + ); + } + } + } + }; + + drawBuilding(width * 0.02, 60, height * 0.35); + drawBuilding(width * 0.1, 75, height * 0.48); + drawBuilding(width * 0.22, 55, height * 0.38); + drawBuilding(width * 0.32, 90, height * 0.55); + drawBuilding(width * 0.48, 65, height * 0.42); + drawBuilding(width * 0.58, 80, height * 0.52); + drawBuilding(width * 0.72, 55, height * 0.36); + drawBuilding(width * 0.82, 70, height * 0.45); +} + +export function drawSmokestacks(dc: DrawContext): void { + const { ctx, width, height, state } = dc; + const parallax = state.mouseX * 0.4; + const time = Date.now() * 0.001; + + const drawSmokestack = (x: number, stackHeight: number) => { + const baseY = height * 0.8; + + ctx.fillStyle = '#263238'; + ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, stackHeight); + + ctx.fillStyle = '#B71C1C'; + ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, 15); + + for (let i = 0; i < 8; i++) { + const smokeY = baseY - stackHeight - i * 20 - Math.sin(time + i * 0.5) * 10; + const smokeRadius = 15 + i * 12; + const offsetX = Math.sin(time * 0.8 + i * 0.3) * 25 * (i / 4); + const alpha = 0.6 - i * 0.06; + + ctx.fillStyle = `rgba(60, 60, 60, ${alpha})`; + ctx.beginPath(); + ctx.arc(x + parallax + offsetX, smokeY, smokeRadius, 0, Math.PI * 2); + ctx.fill(); + } + }; + + drawSmokestack(width * 0.15, height * 0.3); + drawSmokestack(width * 0.55, height * 0.35); + drawSmokestack(width * 0.85, height * 0.28); +} + +export function drawSmog(dc: DrawContext): void { + const { ctx, width, height } = dc; + const time = Date.now() * 0.0003; + + for (let layer = 0; layer < 3; layer++) { + const gradient = ctx.createLinearGradient(0, height * 0.3, 0, height * 0.7); + const alpha = 0.15 + layer * 0.08; + gradient.addColorStop(0, `rgba(80, 80, 80, 0)`); + gradient.addColorStop(0.5, `rgba(100, 90, 80, ${alpha})`); + gradient.addColorStop(1, `rgba(80, 80, 80, 0)`); + + ctx.fillStyle = gradient; + + ctx.beginPath(); + ctx.moveTo(-100, height * 0.6); + for (let x = -100; x <= width + 100; x += 50) { + const y = height * 0.45 + Math.sin((x + time * 100 + layer * 500) * 0.005) * 40; + ctx.lineTo(x, y); + } + ctx.lineTo(width + 100, height * 0.7); + ctx.lineTo(-100, height * 0.7); + ctx.closePath(); + ctx.fill(); + } +} + +export function drawTrafficJam(dc: DrawContext): void { + const { ctx, width, height } = dc; + + ctx.fillStyle = '#2E2E2E'; + ctx.fillRect(0, height * 0.82, width, height * 0.08); + + ctx.strokeStyle = 'rgba(200, 180, 100, 0.4)'; + ctx.lineWidth = 2; + ctx.setLineDash([20, 30]); + ctx.beginPath(); + ctx.moveTo(0, height * 0.86); + ctx.lineTo(width, height * 0.86); + ctx.stroke(); + ctx.setLineDash([]); + + const drawCar = (x: number, y: number, color: string) => { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.roundRect(x, y, 35, 14, 2); + ctx.fill(); + + ctx.fillStyle = 'rgba(100, 100, 100, 0.5)'; + ctx.fillRect(x + 8, y - 7, 19, 8); + + ctx.fillStyle = '#1a1a1a'; + ctx.beginPath(); + ctx.arc(x + 8, y + 14, 4, 0, Math.PI * 2); + ctx.arc(x + 27, y + 14, 4, 0, Math.PI * 2); + ctx.fill(); + }; + + const carColors = ['#616161', '#424242', '#757575', '#546E7A', '#455A64']; + for (let i = 0; i < 15; i++) { + const lane = i % 2; + const x = i * 55 + 20; + const y = height * 0.83 + lane * 35; + drawCar(x, y, carColors[i % carColors.length]); + } + + ctx.fillStyle = 'rgba(255, 0, 0, 0.4)'; + for (let i = 0; i < 15; i++) { + const x = i * 55 + 48; + const y = height * 0.835 + (i % 2) * 35 + 7; + ctx.beginPath(); + ctx.arc(x, y, 3, 0, Math.PI * 2); + ctx.fill(); + } +} + +export function drawDebris(dc: DrawContext): void { + const { ctx, width, height } = dc; + + ctx.fillStyle = '#5D4037'; + + for (let i = 0; i < 12; i++) { + const x = Math.random() * width; + const y = height * 0.81 + Math.random() * 8; + const size = 3 + Math.random() * 5; + + ctx.beginPath(); + ctx.rect(x, y, size, size * 0.7); + ctx.fill(); + } +} + +export function drawPollutedCityScene(dc: DrawContext): void { + drawSmog(dc); + drawSmoggyBuildings(dc); + drawSmokestacks(dc); + drawDebris(dc); + drawTrafficJam(dc); +} diff --git a/frontend/src/lib/ts/parallax/types.ts b/frontend/src/lib/ts/parallax/types.ts new file mode 100644 index 0000000..c4a69e3 --- /dev/null +++ b/frontend/src/lib/ts/parallax/types.ts @@ -0,0 +1,44 @@ +export interface ParallaxState { + scrollY: number; + innerHeight: number; + mouseX: number; + mouseY: number; + progress: number; + sceneType: SceneType; + blendToScene?: SceneType; + blendProgress?: number; +} + +export type SceneType = + | 'eco' + | 'industrial' + | 'forest' + | 'deforestation' + | 'ocean' + | 'oilRig' + | 'city' + | 'pollutedCity' + | 'transition'; + +export interface DrawContext { + ctx: CanvasRenderingContext2D; + width: number; + height: number; + state: ParallaxState; +} + +export interface SceneColors { + skyTop: string; + skyBottom: string; + sun: string; + sunGlow: string; + mountainFar: string; + mountainMid: string; + hillFront: string; + treeDark: string; + treeLight: string; + ground: string; + water: string; + cloud: string; + accent: string; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index f0c9806..88f562d 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,13 +1,16 @@ -{#if isApp} -
- {@render children()} -
- + + Ethix - Truth in every scan + + + +{#if isMobile} +
+
+
+ {@render children()} +
+
+ +
{:else}
@@ -137,6 +130,13 @@
{/if} +{#if isCameraActive} + (isCameraActive = false)} + onScanComplete={handleScanComplete} + /> +{/if} + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index eb86122..3b335dd 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,895 +1,39 @@ - -
- - + + Ethix - Home + + - -
-
-
- - See the real impact -
-

- Know What
You Buy. -

-

- Scan a product. See if it's actually good for the planet. Find - better alternatives if it's not. Simple as that. -

- -
- - -
-
-
- {#key scoreIndex} -
- {scores[scoreIndex].label} -
- {/key} -
-
-
- - -
- {#key scoreIndex} -
- -
-
- {scores[scoreIndex].label} -
-
- {scores[scoreIndex].score} -
-
-
- {/key} -
-
-
-
- - -
- - -
- -
- {#each stats as stat} -
-
{stat.value}
-
{stat.label}
-
- {/each} -
-
-
- - - -
-

How It Works

-

- Tools to help you shop smarter. -

-
-
- {#each features as feature} -
-
- - - -
-

{feature.title}

-

{feature.desc}

-
- {/each} -
-
- - - -
-

Latest News

-

- Updates from the world of sustainability. -

-
-
- {#each news as item (item.id)} -
-
- -
-
{item.date}
-

{item.title}

-

{item.desc}

- - Read - - -
- {/each} -
-
+
+
- -
-
-
-

Scan History

-

Your recent findings

-
- -
- {#each scanHistory as item} -
-
- -
-
-

{item.name}

-

{item.date}

-
-
- {item.severity} -
-
- {/each} -
-
+
+
diff --git a/frontend/src/routes/catalogue/+page.svelte b/frontend/src/routes/catalogue/+page.svelte index d86e5dd..eae39a3 100644 --- a/frontend/src/routes/catalogue/+page.svelte +++ b/frontend/src/routes/catalogue/+page.svelte @@ -1,5 +1,6 @@ + + Ethix - Product Catalogue + + +
+
-
-

Product Database

-

Search our verified sustainability ratings

+
+
+

Product Database

+

+ Search our verified sustainability ratings +

+
+ +
+
+ +
+ +
+ +
+ {#each categories as category} + + {/each} +
- -
- - -
- - -
- {#each categories as category} - - {/each} -
- -
{#each filteredProducts as product}
- +
- +

{product.name}

{product.brand}

-
+
{product.score}
@@ -139,7 +159,6 @@ .page-wrapper { width: 100%; min-height: 100vh; - background-color: #000000; overflow-x: hidden; position: relative; } @@ -148,6 +167,10 @@ display: none; } + .bg-overlay { + display: none; + } + .content-container { position: relative; z-index: 10; @@ -156,81 +179,91 @@ margin: 0 auto; } + .glass-header { + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 32px; + padding: 40px; + margin-bottom: 40px; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + } + .header { - text-align: left; margin-bottom: 32px; padding: 0 12px; + text-align: center; } .page-title { color: white; - font-size: 48px; + font-size: 42px; font-weight: 900; margin: 0; letter-spacing: -2px; } .subtitle { - color: #b3b3b3; + color: rgba(255, 255, 255, 0.7); font-size: 16px; margin: 8px 0 0 0; font-weight: 500; } - /* Search Bar */ .search-container { position: relative; margin-bottom: 24px; - padding: 0 12px; + max-width: 600px; + margin: 0 auto 32px; } .search-input { width: 100%; - background: #2a2a2a; - border: none; - border-radius: 500px; /* Capsule */ - padding: 14px 48px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50px; + padding: 16px 20px 16px 52px; color: white; font-size: 16px; font-weight: 500; outline: none; - transition: background 0.2s; + transition: all 0.2s; } .search-input:focus { - background: #333333; - box-shadow: 0 0 0 2px white; /* Focus ring */ + background: rgba(255, 255, 255, 0.15); + border-color: #34d399; } .search-input::placeholder { - color: #b3b3b3; + color: rgba(255, 255, 255, 0.5); } - .search-icon { + .search-icon-wrapper { position: absolute; - left: 32px; + left: 20px; top: 50%; transform: translateY(-50%); - color: #b3b3b3; - font-size: 20px; + color: rgba(255, 255, 255, 0.6); + display: flex; + align-items: center; pointer-events: none; } - /* Filters */ .filter-bar { display: flex; - gap: 12px; - margin-bottom: 32px; + justify-content: center; + gap: 10px; flex-wrap: wrap; - padding: 0 12px; } .filter-chip { - background: #2a2a2a; /* Pill bg */ + background: rgba(255, 255, 255, 0.08); color: white; - border: none; - padding: 8px 16px; - border-radius: 500px; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 10px 20px; + border-radius: 50px; font-weight: 600; font-size: 14px; cursor: pointer; @@ -238,27 +271,31 @@ } .filter-chip:hover { - background: #333333; + background: rgba(255, 255, 255, 0.15); } .filter-chip.active { - background: #1ed760; - color: #000000; + background: linear-gradient(135deg, #22c55e, #16a34a); + color: white; + border-color: transparent; + box-shadow: 0 4px 16px rgba(34, 197, 94, 0.3); } - /* Grid */ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 24px; + gap: 20px; padding: 0 12px; } .product-card { - background: #181818; /* Spotify Card */ - border-radius: 8px; - padding: 16px; - transition: background-color 0.3s ease; + background: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 20px; + transition: all 0.3s ease; display: flex; flex-direction: column; cursor: pointer; @@ -266,19 +303,20 @@ } .product-card:hover { - background-color: #282828; + background: rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.15); + transform: translateY(-4px); } .card-image-placeholder { width: 100%; aspect-ratio: 1; - background: #333333; - border-radius: 4px; /* Slightly rounded images */ + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; - box-shadow: 0 8px 24px rgba(0,0,0,0.5); } .product-info { @@ -296,27 +334,27 @@ } .product-brand { - color: #b3b3b3; + color: rgba(255, 255, 255, 0.5); font-size: 14px; margin: 0; } .score-badge { position: absolute; - top: 24px; - right: 24px; - width: 32px; - height: 32px; + top: 28px; + right: 28px; + width: 36px; + height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 8px rgba(0,0,0,0.3); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .score-text { - color: #000000; - font-weight: 900; + color: white; + font-weight: 800; font-size: 12px; } @@ -330,9 +368,20 @@ height: 100%; pointer-events: none; } + } - .page-wrapper { - background: transparent; + @media (max-width: 767px) { + .content-container { + padding: 60px 16px 100px; + } + + .page-title { + font-size: 32px; + } + + .product-grid { + grid-template-columns: repeat(2, 1fr); + gap: 12px; } } diff --git a/frontend/src/routes/chat/+page.svelte b/frontend/src/routes/chat/+page.svelte index 38c87f2..86d84a2 100644 --- a/frontend/src/routes/chat/+page.svelte +++ b/frontend/src/routes/chat/+page.svelte @@ -1,7 +1,7 @@ + + Ethix - Chat Assistant + + +
-
- -
-
-
- -
-
-

Ethix Assistant

+
+
+
+
+ +
- -
-
- {#each messages as msg (msg.id)} -
-

{msg.text}

-
- {/each} -
- -
- - -
+

Ethix Assistant

+
+ + Powered by Gemini
- + +
+
+ {#each messages as msg (msg.id)} +
+

{msg.text}

+
+ {/each} +
+ +
+ + +
+
+
diff --git a/frontend/src/routes/community/+page.svelte b/frontend/src/routes/community/+page.svelte index 8d1eb49..6b46197 100644 --- a/frontend/src/routes/community/+page.svelte +++ b/frontend/src/routes/community/+page.svelte @@ -1,6 +1,5 @@
@@ -9,107 +8,93 @@
- -
-

Why We Exist

-

Our Mission & Goal

+
+

Why We Exist

+

Our Mission & Goal

+
+ +
+
+
+ +
+

The Problem

+

+ "Greenwashing" is everywhere. Companies spend millions to + make you believe their products are sustainable, when often + they are not. +

+
+ 53% + of green claims are vague or misleading +
-
- -
-
- - - -
-

The Problem

-

- "Greenwashing" is everywhere. Companies spend millions - to make you believe their products are sustainable, when - often they are not. -

-
- 53% - of green claims are vague or misleading -
+
+
+
- - -
-
- - - -
-

The Solution

-

- We believe in radical transparency. By using AI to - analyze packaging and verify claims, we give power back - to you, the consumer. -

-
    -
  • - - Instant Fact-Checking -
  • -
  • - - Unbiased Eco-Ratings -
  • -
  • - - Real Sustainable Alternatives -
  • -
-
- - -
-
-

Our Ultimate Goal

+

The Solution

+

+ We believe in radical transparency. By using AI to analyze + packaging and verify claims, we give power back to you, the + consumer. +

+
    +
  • -
-

- To create a world where sustainability is the default, not a luxury. Where every purchase you make pushes - the industry toward a cleaner, ethical future. -

-
+ Instant Fact-Checking + +
  • + + Unbiased Eco-Ratings +
  • +
  • + + Real Sustainable Alternatives +
  • +
    - + +
    +
    + +

    Our Ultimate Goal

    +
    +

    + To create a world where sustainability is the default, not a luxury. Where every purchase you make pushes the + industry toward a cleaner, ethical future. +

    +
    +
    @@ -117,7 +102,6 @@ .page-wrapper { width: 100%; min-height: 100vh; - background-color: #000000; overflow-x: hidden; position: relative; } @@ -129,19 +113,18 @@ .content-container { position: relative; z-index: 10; - padding: 80px 24px 120px; - max-width: 800px; + padding: 100px 24px 120px; + max-width: 900px; margin: 0 auto; } - .header { + .header-card { text-align: center; - margin-bottom: 40px; - padding: 0 20px; + margin-bottom: 48px; } .page-title { - color: #000000; + color: white; font-size: 48px; font-weight: 900; margin: 0; @@ -149,10 +132,10 @@ } .subtitle { - color: #4b5563; - font-size: 16px; - margin: 16px 0 0 0; - font-weight: 700; + color: rgba(255, 255, 255, 0.5); + font-size: 14px; + margin: 12px 0 0 0; + font-weight: 600; text-transform: uppercase; letter-spacing: 2px; } @@ -163,13 +146,20 @@ gap: 24px; } - .card { - background: #a1a1aa; /* Light Grey Card */ - padding: 40px; - border-radius: 8px; - box-shadow: none; - position: relative; - overflow: hidden; + .glass-card { + background: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + padding: 36px; + transition: all 0.3s ease; + } + + .glass-card:hover { + background: rgba(0, 0, 0, 0.45); + border-color: rgba(255, 255, 255, 0.15); } .mission-card { @@ -179,7 +169,6 @@ } .goal-card { - background: #a1a1aa; text-align: center; align-items: center; display: flex; @@ -187,25 +176,25 @@ } .icon-circle { - width: 60px; - height: 60px; + width: 64px; + height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - margin-bottom: 24px; + margin-bottom: 20px; } .problem-icon { - background: rgba(233, 20, 41, 0.1); + background: rgba(239, 68, 68, 0.15); } .solution-icon { - background: rgba(34, 197, 94, 0.1); /* Eco Green tint */ + background: rgba(34, 197, 94, 0.15); } .card-title { - color: #1a1a1a; + color: white; font-size: 24px; font-weight: 700; margin: 0 0 16px 0; @@ -217,33 +206,33 @@ } .card-desc { - color: #333333; + color: rgba(255, 255, 255, 0.7); font-size: 16px; line-height: 1.6; margin: 0 0 24px 0; } .stat-highlight { - background: #374151; /* Darker grey */ - padding: 16px 24px; - border-radius: 8px; + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); + padding: 16px 20px; + border-radius: 16px; display: flex; align-items: center; gap: 16px; width: 100%; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .stat-number { - font-size: 32px; + font-size: 36px; font-weight: 900; - color: #e91429; + color: #ef4444; } .stat-label { - color: #f3f4f6; /* Light text on dark bg */ + color: rgba(255, 255, 255, 0.8); font-size: 14px; - font-weight: 600; + font-weight: 500; line-height: 1.4; } @@ -258,29 +247,33 @@ display: flex; align-items: center; gap: 12px; - margin-bottom: 16px; - font-size: 16px; - color: #1a1a1a; - font-weight: 700; + margin-bottom: 14px; + font-size: 15px; + color: white; + font-weight: 600; + } + + .benefit-list li:last-child { + margin-bottom: 0; } .header-row { display: flex; align-items: center; - gap: 12px; - margin-bottom: 24px; + gap: 14px; + margin-bottom: 20px; } .goal-text { - color: #333333; + color: rgba(255, 255, 255, 0.7); font-size: 18px; - line-height: 1.6; - margin-bottom: 0; + line-height: 1.7; + margin: 0; max-width: 600px; } .highlight { - color: #166534; + color: #4ade80; font-weight: 700; } @@ -295,19 +288,20 @@ pointer-events: none; } - .page-wrapper { - background: transparent; + .grid-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + } + + .goal-card { + grid-column: span 2; } } @media (max-width: 767px) { .content-container { - padding: 40px 20px 100px; - } - - .header { - padding: 0; - margin-bottom: 30px; + padding: 60px 20px 100px; } .page-title { diff --git a/frontend/src/routes/news/+page.svelte b/frontend/src/routes/news/+page.svelte index 7bd930d..54a46a9 100644 --- a/frontend/src/routes/news/+page.svelte +++ b/frontend/src/routes/news/+page.svelte @@ -1,113 +1,225 @@ -
    -
    +
    +
    + +
    + +

    Eco News

    Latest sustainability updates

    -
    +
    {#each news as item (item.id)} -
    -
    -

    {item.date} • Trending

    +
    +
    +
    + +
    + {item.tag} +
    +
    {item.date}

    {item.title}

    {item.desc}

    -
    + + Read more + + + {/each}
    diff --git a/frontend/src/routes/report/+page.svelte b/frontend/src/routes/report/+page.svelte index 8d43fa3..5c85108 100644 --- a/frontend/src/routes/report/+page.svelte +++ b/frontend/src/routes/report/+page.svelte @@ -1,6 +1,5 @@