mirror of
https://github.com/SirBlobby/Hoya26.git
synced 2026-02-03 19:24:34 -05:00
Database and Reports Update
This commit is contained in:
@@ -1,7 +1,112 @@
|
||||
# Tauri + SvelteKit + TypeScript
|
||||
# Ethix Frontend
|
||||
|
||||
This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite.
|
||||
A SvelteKit web application for the Ethix greenwashing detection platform. Scan products, report misleading environmental claims, and chat with an AI assistant about sustainability.
|
||||
|
||||
## Recommended IDE Setup
|
||||
## Technology Stack
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Framework | SvelteKit |
|
||||
| UI Library | Svelte 5 |
|
||||
| Language | TypeScript |
|
||||
| Styling | TailwindCSS 4 |
|
||||
| Build Tool | Vite |
|
||||
| Desktop App | Tauri |
|
||||
| Icons | Iconify |
|
||||
| 3D Graphics | Three.js |
|
||||
| Markdown | marked |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ or Bun
|
||||
- Backend server running on `http://localhost:5000`
|
||||
|
||||
## Installation
|
||||
|
||||
Using Bun (recommended):
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
Or using npm:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
# or
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:5173`.
|
||||
|
||||
## Building
|
||||
|
||||
### Web Build
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
# or
|
||||
npm run build
|
||||
```
|
||||
|
||||
Preview the production build:
|
||||
|
||||
```bash
|
||||
bun run preview
|
||||
# or
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Desktop Build (Tauri)
|
||||
|
||||
```bash
|
||||
bun run tauri build
|
||||
# or
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Home Page
|
||||
|
||||
- Responsive design with separate mobile and web layouts
|
||||
- Animated parallax landscape background
|
||||
- Quick access to all main features
|
||||
|
||||
### AI Chat Assistant
|
||||
|
||||
- Powered by Google Gemini with RAG context
|
||||
- Real-time conversation interface
|
||||
- Sustainability and greenwashing expertise
|
||||
- Message history within session
|
||||
|
||||
### Greenwashing Report Submission
|
||||
|
||||
- Two report types: Product Incident or Company Report
|
||||
- Image upload for product evidence
|
||||
- PDF upload for company sustainability reports
|
||||
- Real-time analysis progress indicator
|
||||
- Structured verdict display with confidence levels
|
||||
|
||||
### Catalogue Browser
|
||||
|
||||
- Browse company sustainability reports
|
||||
- View user-submitted incidents
|
||||
- Category filtering (Tech, Energy, Automotive, etc.)
|
||||
- Semantic search functionality
|
||||
- Pagination for large datasets
|
||||
- Detailed modal views for reports and incidents
|
||||
|
||||
### Product Scanner
|
||||
|
||||
- Camera integration for scanning product labels
|
||||
- Brand/logo detection via AI vision
|
||||
- Direct report submission from scan results
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const tabs = [
|
||||
{
|
||||
name: "Goals",
|
||||
route: "/community",
|
||||
route: "/goal",
|
||||
icon: "ri:flag-fill",
|
||||
activeColor: "#34d399",
|
||||
},
|
||||
|
||||
@@ -43,7 +43,11 @@
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo-icon">
|
||||
<Icon icon="ri:leaf-fill" width="24" />
|
||||
<img
|
||||
src="/ethix-logo.png"
|
||||
alt="Ethix Logo"
|
||||
class="logo-img"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="app-name">Ethix</h1>
|
||||
@@ -213,13 +217,17 @@
|
||||
.logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #4ade80, #22c55e);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #052e16;
|
||||
box-shadow: 0 4px 16px rgba(74, 222, 128, 0.4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
|
||||
@@ -71,10 +71,10 @@
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="avatar">
|
||||
<Icon
|
||||
icon="ri:seedling-fill"
|
||||
width="24"
|
||||
style="color: #34d399;"
|
||||
<img
|
||||
src="/ethix-logo.png"
|
||||
alt="Ethix Logo"
|
||||
class="avatar-logo"
|
||||
/>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
@@ -222,6 +222,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,16 +24,15 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-black/60 backdrop-blur-2xl border border-white/15 rounded-4xl p-12 lg:p-14 mb-12 shadow-2xl shadow-black/50"
|
||||
class="bg-black/40 backdrop-blur-2xl border border-white/10 rounded-4xl p-12 lg:p-14 mb-12 shadow-2xl shadow-black/30"
|
||||
>
|
||||
|
||||
<div class="mb-10 px-4 text-center">
|
||||
<h1
|
||||
class="text-white text-[42px] lg:text-[48px] font-black m-0 tracking-[-2px]"
|
||||
>
|
||||
Sustainability Database
|
||||
</h1>
|
||||
<p class="text-white/80 text-base lg:text-lg mt-3 font-medium">
|
||||
<p class="text-white/90 text-base lg:text-lg mt-3 font-medium">
|
||||
{#if viewMode === "company"}
|
||||
Search within verified company reports and impact assessments
|
||||
{:else}
|
||||
@@ -42,19 +41,18 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center justify-center gap-5 my-8">
|
||||
<span
|
||||
class="flex items-center gap-2 text-sm lg:text-base font-semibold transition-all duration-300 {viewMode ===
|
||||
'company'
|
||||
? 'text-emerald-400'
|
||||
: 'text-white/50'}"
|
||||
: 'text-white/60'}"
|
||||
>
|
||||
<Icon icon="ri:building-2-line" width="16" />
|
||||
Company
|
||||
</span>
|
||||
<button
|
||||
class="relative w-16 h-8 bg-white/10 border-none rounded-full cursor-pointer transition-all duration-300 p-0 hover:bg-white/15 shadow-inner"
|
||||
class="relative w-16 h-8 bg-white/10 border-none rounded-full cursor-pointer transition-all duration-300 p-0 hover:bg-white/20 shadow-inner"
|
||||
onclick={() =>
|
||||
(viewMode = viewMode === "company" ? "user" : "company")}
|
||||
aria-label="Toggle between company and user reports"
|
||||
@@ -70,7 +68,7 @@
|
||||
class="flex items-center gap-2 text-sm lg:text-base font-semibold transition-all duration-300 {viewMode ===
|
||||
'user'
|
||||
? 'text-emerald-400'
|
||||
: 'text-white/50'}"
|
||||
: 'text-white/60'}"
|
||||
>
|
||||
<Icon icon="ri:user-voice-line" width="16" />
|
||||
User Reports
|
||||
@@ -86,7 +84,7 @@
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-black/30 border border-white/15 rounded-full py-4 lg:py-5 pl-14 pr-6 text-white text-base lg:text-lg font-medium outline-none transition-all duration-200 focus:bg-black/40 focus:border-emerald-400 placeholder:text-white/50 shadow-inner"
|
||||
class="w-full bg-white/10 border border-white/10 rounded-full py-4 lg:py-5 pl-14 pr-6 text-white text-base lg:text-lg font-medium outline-none transition-all duration-200 focus:bg-white/15 focus:border-emerald-400 placeholder:text-white/40 shadow-inner"
|
||||
placeholder="Search for companies, topics (e.g., 'emissions')..."
|
||||
bind:value={searchQuery}
|
||||
oninput={onSearchInput}
|
||||
@@ -99,7 +97,7 @@
|
||||
class="px-7 py-3 rounded-full text-sm lg:text-base font-semibold cursor-pointer transition-all duration-200 border {selectedCategory ===
|
||||
category
|
||||
? 'bg-emerald-500 border-emerald-500 text-emerald-950 shadow-[0_4px_20px_rgba(34,197,94,0.4)]'
|
||||
: 'bg-black/30 backdrop-blur-sm border-white/15 text-white/70 hover:bg-black/40 hover:text-white hover:-translate-y-0.5 hover:shadow-lg'}"
|
||||
: 'bg-white/10 backdrop-blur-sm border-white/10 text-white/80 hover:bg-white/20 hover:text-white hover:-translate-y-0.5 hover:shadow-lg'}"
|
||||
onclick={() => (selectedCategory = category)}
|
||||
>
|
||||
{category}
|
||||
|
||||
@@ -43,57 +43,63 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="group flex items-center gap-6 lg:gap-8 bg-black/60 backdrop-blur-2xl border border-white/15 rounded-3xl p-7 lg:p-8 w-full text-left cursor-pointer transition-all duration-300 hover:bg-black/70 hover:border-emerald-500/50 hover:-translate-y-1 hover:scale-[1.01] hover:shadow-2xl hover:shadow-black/50 relative overflow-hidden outline-none"
|
||||
class="group flex items-center gap-6 lg:gap-8 bg-black/40 backdrop-blur-2xl border border-white/10 rounded-3xl p-7 lg:p-8 w-full text-left cursor-pointer transition-all duration-300 hover:bg-black/50 hover:border-emerald-500/40 hover:-translate-y-1 hover:scale-[1.005] hover:shadow-xl hover:shadow-black/30 relative overflow-hidden outline-none"
|
||||
onclick={() => openReport(report)}
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 lg:w-[4.375rem] lg:h-[4.375rem] bg-emerald-500/15 backdrop-blur-sm rounded-[1.125rem] flex items-center justify-center shrink-0 transition-all duration-300 group-hover:bg-emerald-500 group-hover:-rotate-6 group-hover:scale-110"
|
||||
class="w-16 h-16 lg:w-[4.375rem] lg:h-[4.375rem] bg-emerald-500/10 backdrop-blur-sm rounded-[1.125rem] flex items-center justify-center shrink-0 transition-all duration-300 group-hover:bg-emerald-500 group-hover:-rotate-3 group-hover:scale-105 border border-white/5 group-hover:border-emerald-400/50"
|
||||
>
|
||||
<Icon icon={fileDetails.icon} width="32" class="text-white" />
|
||||
<Icon
|
||||
icon={fileDetails.icon}
|
||||
width="32"
|
||||
class="text-emerald-100 group-hover:text-white transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-2.5">
|
||||
<h3 class="text-white text-xl lg:text-[22px] font-extrabold m-0">
|
||||
<h3
|
||||
class="text-white text-xl lg:text-[22px] font-bold m-0 tracking-tight"
|
||||
>
|
||||
{report.company_name}
|
||||
</h3>
|
||||
<span
|
||||
class="text-emerald-300 bg-emerald-500/15 backdrop-blur-sm px-3 py-1 rounded-lg text-[12px] lg:text-[13px] font-bold"
|
||||
class="text-emerald-300 bg-emerald-500/15 backdrop-blur-sm px-3 py-1 rounded-lg text-[12px] lg:text-[13px] font-bold border border-emerald-500/10"
|
||||
>{report.year}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if report.snippet}
|
||||
<p
|
||||
class="text-white/70 text-sm lg:text-base leading-relaxed mb-4 line-clamp-2 m-0"
|
||||
class="text-white/80 text-sm lg:text-base leading-relaxed mb-4 line-clamp-2 m-0 font-medium"
|
||||
>
|
||||
{@html report.snippet.replace(
|
||||
new RegExp(searchQuery || "", "gi"),
|
||||
(match) =>
|
||||
`<span class="text-emerald-300 bg-emerald-500/25 px-1 rounded font-semibold">${match}</span>`,
|
||||
`<span class="text-emerald-200 bg-emerald-500/30 px-1 rounded font-bold">${match}</span>`,
|
||||
)}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-white/50 text-sm lg:text-base mb-4 m-0">
|
||||
<p class="text-white/60 text-sm lg:text-base mb-4 m-0 font-medium">
|
||||
{report.sector} Sector • Impact Report
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2.5">
|
||||
<span
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] lg:text-[12px] font-bold tracking-tight bg-white/10 backdrop-blur-sm text-white/70 max-w-xs truncate"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] lg:text-[12px] font-bold tracking-tight bg-white/5 backdrop-blur-sm text-white/70 max-w-xs truncate border border-white/5"
|
||||
title={report.filename}
|
||||
>
|
||||
<Icon icon={fileDetails.icon} width="15" />
|
||||
{fileDetails.type}
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] lg:text-[12px] font-bold tracking-tight bg-emerald-500/15 backdrop-blur-sm text-emerald-300"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] lg:text-[12px] font-bold tracking-tight bg-emerald-500/10 backdrop-blur-sm text-emerald-300 border border-emerald-500/10"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="14"
|
||||
class="text-emerald-500"
|
||||
class="text-emerald-400"
|
||||
/>
|
||||
Analyzed
|
||||
</span>
|
||||
@@ -101,18 +107,21 @@
|
||||
</div>
|
||||
|
||||
{#if report.greenwashing_score}
|
||||
<div class="text-center ml-5 lg:ml-6">
|
||||
<div
|
||||
class="text-center ml-5 lg:ml-6 group-hover:translate-x-1 transition-transform duration-300"
|
||||
>
|
||||
<div
|
||||
class="w-14 h-14 lg:w-[3.875rem] lg:h-[3.875rem] rounded-xe flex items-center justify-center mb-2 rounded-2xl shadow-lg transition-transform group-hover:scale-110 {getScoreColor(
|
||||
class="w-14 h-14 lg:w-[3.875rem] lg:h-[3.875rem] rounded-xe flex items-center justify-center mb-2 rounded-2xl shadow-lg {getScoreColor(
|
||||
report.greenwashing_score,
|
||||
)}"
|
||||
)} ring-4 ring-black/20"
|
||||
>
|
||||
<span class="text-emerald-950 text-[19px] lg:text-[21px] font-black"
|
||||
<span
|
||||
class="text-emerald-950 text-[19px] lg:text-[21px] font-black"
|
||||
>{Math.round(Number(report.greenwashing_score))}</span
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
class="text-white/50 text-[10px] lg:text-[11px] font-extrabold uppercase tracking-widest"
|
||||
class="text-white/60 text-[10px] lg:text-[11px] font-bold uppercase tracking-widest"
|
||||
>Trust Score</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,182 +1,182 @@
|
||||
import type { SceneColors, SceneType, ParallaxState } from './types';
|
||||
|
||||
export const SCENE_COLORS: Record<Exclude<SceneType, 'transition'>, 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;
|
||||
}
|
||||
import type { SceneColors, SceneType, ParallaxState } from './types';
|
||||
|
||||
export const SCENE_COLORS: Record<Exclude<SceneType, 'transition'>, 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;
|
||||
}
|
||||
|
||||
@@ -1,151 +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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,121 @@
|
||||
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<Exclude<SceneType, 'transition'>, (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);
|
||||
}
|
||||
}
|
||||
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<Exclude<SceneType, 'transition'>, (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 p = state.progress;
|
||||
|
||||
// Eco fades out quickly: 1.0 -> 0.0 from progress 0.0 to 0.45
|
||||
const ecoOpacity = Math.max(0, 1 - (p / 0.45));
|
||||
|
||||
// Industrial fades in later: 0.0 -> 1.0 from progress 0.55 to 1.0
|
||||
const industrialOpacity = Math.max(0, (p - 0.55) / 0.45);
|
||||
|
||||
if (ecoOpacity > 0.01) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = ecoOpacity;
|
||||
drawEcoScene(dc);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Transition Fog: Peaks at progress 0.5 to bridge the gap between scenes
|
||||
// Increased max opacity to 0.9 to fully hide the "swap"
|
||||
const fogOpacity = Math.sin(p * Math.PI) * 0.9;
|
||||
|
||||
if (fogOpacity > 0.01) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = fogOpacity;
|
||||
const gradient = ctx.createLinearGradient(0, height * 0.2, 0, height);
|
||||
gradient.addColorStop(0, 'rgba(230, 230, 230, 0)'); // Transparent top
|
||||
gradient.addColorStop(0.3, 'rgba(200, 200, 200, 0.6)'); // Hazy middle
|
||||
gradient.addColorStop(1, 'rgba(160, 160, 160, 1.0)'); // Thick bottom
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (industrialOpacity > 0.01) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = industrialOpacity;
|
||||
drawIndustrialScene(dc);
|
||||
ctx.restore();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,137 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,145 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,168 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,130 +1,154 @@
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
function drawFog(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
// Drifting fog banks
|
||||
const drawFogLayer = (y: number, speed: number, opacity: number) => {
|
||||
const drift = (time * speed) % width;
|
||||
const xOffset = drift > 0 ? drift - width : drift;
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, y, 0, y + 150);
|
||||
gradient.addColorStop(0, 'rgba(120, 120, 120, 0)');
|
||||
gradient.addColorStop(0.5, `rgba(120, 120, 120, ${opacity})`);
|
||||
gradient.addColorStop(1, 'rgba(120, 120, 120, 0)');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(xOffset, y, width * 2, 150); // Draw wide to handle looping
|
||||
};
|
||||
|
||||
drawFogLayer(height * 0.65, 20, 0.15);
|
||||
drawFogLayer(height * 0.75, -15, 0.2);
|
||||
drawFogLayer(height * 0.55, 10, 0.1);
|
||||
}
|
||||
|
||||
export function drawIndustrialScene(dc: DrawContext): void {
|
||||
drawPowerLines(dc);
|
||||
drawFactories(dc);
|
||||
drawDeadTrees(dc);
|
||||
drawFog(dc);
|
||||
}
|
||||
|
||||
@@ -1,197 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,162 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,176 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,44 +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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
const navLinks = [
|
||||
{ name: "Home", route: "/", icon: "ri:home-4-line" },
|
||||
{ name: "Goal", route: "/community", icon: "ri:flag-2-line" },
|
||||
{ name: "Goal", route: "/goal", icon: "ri:flag-2-line" },
|
||||
{ name: "Chat", route: "/chat", icon: "ri:chat-3-line" },
|
||||
{ name: "Report", route: "/report", icon: "ri:alarm-warning-line" },
|
||||
];
|
||||
@@ -77,6 +77,7 @@
|
||||
name="description"
|
||||
content="Scan products to reveal their true environmental impact. Join the community of eco-conscious shoppers making a difference."
|
||||
/>
|
||||
<link rel="icon" href="/ethix-logo.png" />
|
||||
</svelte:head>
|
||||
|
||||
{#if isMobile}
|
||||
@@ -181,7 +182,9 @@
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
<script lang="ts">
|
||||
import WebHomePage from "$lib/components/WebHomePage.svelte";
|
||||
import MobileHomePage from "$lib/components/MobileHomePage.svelte";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ethix - Home</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Ethix helps you make sustainable choices. Scan products, track your carbon footprint, and find eco-friendly alternatives."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="web-view">
|
||||
<WebHomePage />
|
||||
</div>
|
||||
|
||||
<div class="mobile-view">
|
||||
<MobileHomePage />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.web-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-view {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.web-view {
|
||||
display: block;
|
||||
}
|
||||
.mobile-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
import WebHomePage from "$lib/components/WebHomePage.svelte";
|
||||
import MobileHomePage from "$lib/components/MobileHomePage.svelte";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ethix - Home</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Ethix helps you make sustainable choices. Scan products, track your carbon footprint, and find eco-friendly alternatives."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="web-view">
|
||||
<WebHomePage />
|
||||
</div>
|
||||
|
||||
<div class="mobile-view">
|
||||
<MobileHomePage />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.web-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-view {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.web-view {
|
||||
display: block;
|
||||
}
|
||||
.mobile-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
messages;
|
||||
isLoading;
|
||||
scrollToBottom();
|
||||
@@ -101,12 +100,11 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative z-10 max-w-6xl mx-auto h-full flex flex-col pt-24 pb-6 px-0 md:px-6 md:pt-20 md:pb-10"
|
||||
class="relative z-10 max-w-6xl mx-auto h-full flex flex-col pt-0 pb-20 px-0 md:px-6 md:pt-20 md:pb-10"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col h-full bg-[#0d2e25] md:bg-black/40 md:backdrop-blur-xl border-x md:border border-[#1f473b] md:border-white/10 md:rounded-[32px] overflow-hidden shadow-2xl"
|
||||
>
|
||||
|
||||
<div
|
||||
class="p-3 px-5 border-b border-[#1f473b] md:border-white/10 bg-[#051f18] md:bg-transparent flex items-center gap-3 shrink-0"
|
||||
>
|
||||
@@ -126,7 +124,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="flex-1 flex flex-col overflow-hidden bg-[#0d2e25] md:bg-transparent"
|
||||
>
|
||||
@@ -150,13 +147,12 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
let isLoading = $state(false);
|
||||
let analysisResult = $state<any>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let uploadSuccess = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
const initialName = new URLSearchParams(window.location.search).get(
|
||||
@@ -22,7 +23,9 @@
|
||||
});
|
||||
|
||||
let isValid = $derived(
|
||||
productName.trim().length > 0 && description.trim().length > 0,
|
||||
reportType === "product"
|
||||
? productName.trim().length > 0 && description.trim().length > 0
|
||||
: productName.trim().length > 0 && pdfData !== null,
|
||||
);
|
||||
|
||||
async function pickImage() {
|
||||
@@ -62,7 +65,8 @@
|
||||
input.click();
|
||||
}
|
||||
|
||||
const progressSteps = [
|
||||
// Progress steps for product incident
|
||||
const productProgressSteps = [
|
||||
{ id: 1, label: "Scanning image...", icon: "ri:camera-lens-line" },
|
||||
{
|
||||
id: 2,
|
||||
@@ -78,6 +82,31 @@
|
||||
},
|
||||
];
|
||||
|
||||
// Progress steps for company report upload
|
||||
const companyProgressSteps = [
|
||||
{ id: 1, label: "Decoding PDF...", icon: "ri:file-pdf-2-line" },
|
||||
{
|
||||
id: 2,
|
||||
label: "Extracting text content...",
|
||||
icon: "ri:file-text-line",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "Validating report authenticity...",
|
||||
icon: "ri:shield-check-line",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: "Analyzing environmental claims...",
|
||||
icon: "ri:robot-2-line",
|
||||
},
|
||||
{ id: 5, label: "Saving to database...", icon: "ri:database-2-line" },
|
||||
{ id: 6, label: "Indexing for search...", icon: "ri:search-line" },
|
||||
];
|
||||
|
||||
let progressSteps = $derived(
|
||||
reportType === "product" ? productProgressSteps : companyProgressSteps,
|
||||
);
|
||||
let currentStep = $state(0);
|
||||
|
||||
async function handleSubmit() {
|
||||
@@ -86,44 +115,89 @@
|
||||
isLoading = true;
|
||||
error = null;
|
||||
currentStep = 1;
|
||||
uploadSuccess = false;
|
||||
|
||||
|
||||
const stepInterval = setInterval(() => {
|
||||
if (currentStep < progressSteps.length) {
|
||||
currentStep++;
|
||||
}
|
||||
}, 1500);
|
||||
const stepInterval = setInterval(
|
||||
() => {
|
||||
if (currentStep < progressSteps.length) {
|
||||
currentStep++;
|
||||
}
|
||||
},
|
||||
reportType === "company" ? 2000 : 1500,
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/incidents/submit",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
if (reportType === "company") {
|
||||
// Use the new upload endpoint for company reports
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/reports/upload",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
company_name: productName,
|
||||
pdf_data: pdfData,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_name: productName,
|
||||
description: description,
|
||||
report_type: reportType,
|
||||
image: reportType === "product" ? image : null,
|
||||
pdf_data: reportType === "company" ? pdfData : null,
|
||||
}),
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
clearInterval(stepInterval);
|
||||
currentStep = progressSteps.length;
|
||||
clearInterval(stepInterval);
|
||||
currentStep = progressSteps.length;
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
analysisResult = data;
|
||||
submitted = true;
|
||||
if (data.status === "success") {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
analysisResult = {
|
||||
...data,
|
||||
is_company_report: true,
|
||||
};
|
||||
uploadSuccess = true;
|
||||
submitted = true;
|
||||
} else if (data.status === "rejected") {
|
||||
error =
|
||||
data.message ||
|
||||
"Report was rejected - not a valid environmental report";
|
||||
analysisResult = {
|
||||
rejected: true,
|
||||
validation: data.validation,
|
||||
};
|
||||
} else {
|
||||
error = data.message || "Failed to upload report";
|
||||
}
|
||||
} else {
|
||||
error = data.message || "Failed to submit report";
|
||||
// Original product incident flow
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/incidents/submit",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_name: productName,
|
||||
description: description,
|
||||
report_type: reportType,
|
||||
image: image,
|
||||
pdf_data: null,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
clearInterval(stepInterval);
|
||||
currentStep = progressSteps.length;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
analysisResult = data;
|
||||
submitted = true;
|
||||
} else {
|
||||
error = data.message || "Failed to submit report";
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(stepInterval);
|
||||
@@ -134,6 +208,18 @@
|
||||
currentStep = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
submitted = false;
|
||||
uploadSuccess = false;
|
||||
analysisResult = null;
|
||||
error = null;
|
||||
productName = "";
|
||||
description = "";
|
||||
image = null;
|
||||
pdfData = null;
|
||||
pdfName = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page-wrapper">
|
||||
@@ -144,7 +230,91 @@
|
||||
<div class="content-container">
|
||||
{#if submitted && analysisResult}
|
||||
<div class="glass-card success-card">
|
||||
{#if analysisResult.is_greenwashing}
|
||||
{#if analysisResult.is_company_report}
|
||||
<!-- Company Report Upload Success -->
|
||||
<div class="icon-circle success">
|
||||
<iconify-icon
|
||||
icon="ri:file-check-fill"
|
||||
width="60"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<h2 class="success-title">Report Verified & Uploaded</h2>
|
||||
|
||||
<div class="analysis-result">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Company:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.stats?.company_name ||
|
||||
"Unknown"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Year:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.stats?.year || "N/A"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Sector:</span>
|
||||
<span class="result-value badge"
|
||||
>{analysisResult.stats?.sector || "Other"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Report Type:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.stats?.report_type ||
|
||||
"Unknown"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Pages:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.stats?.page_count || 0}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Chunks Created:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.stats?.chunks_created ||
|
||||
0}</span
|
||||
>
|
||||
</div>
|
||||
{#if analysisResult.validation?.key_topics}
|
||||
<div class="result-item full-width">
|
||||
<span class="result-label">Key Topics:</span>
|
||||
<div class="topics-list">
|
||||
{#each analysisResult.validation.key_topics.slice(0, 5) as topic}
|
||||
<span class="topic-tag">{topic}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if analysisResult.validation?.reasoning}
|
||||
<div class="result-item full-width">
|
||||
<span class="result-label">AI Assessment:</span>
|
||||
<p class="result-text">
|
||||
{analysisResult.validation.reasoning}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="/catalogue" class="btn-primary">
|
||||
<iconify-icon icon="ri:folder-open-line" width="18"
|
||||
></iconify-icon>
|
||||
View in Catalogue
|
||||
</a>
|
||||
<button class="back-btn" onclick={resetForm}>
|
||||
<iconify-icon icon="ri:add-line" width="20"
|
||||
></iconify-icon>
|
||||
Upload Another
|
||||
</button>
|
||||
</div>
|
||||
{:else if analysisResult.is_greenwashing}
|
||||
<!-- Greenwashing Detected -->
|
||||
<div class="icon-circle warning">
|
||||
<iconify-icon
|
||||
icon="ri:alert-fill"
|
||||
@@ -155,34 +325,25 @@
|
||||
<h2 class="success-title warning-text">
|
||||
Greenwashing Detected!
|
||||
</h2>
|
||||
{:else}
|
||||
<div class="icon-circle success">
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="60"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<h2 class="success-title">Report Analyzed</h2>
|
||||
{/if}
|
||||
|
||||
<div class="analysis-result">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Verdict:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.analysis?.verdict || "N/A"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Confidence:</span>
|
||||
<span
|
||||
class="result-value badge {analysisResult.analysis
|
||||
?.confidence}"
|
||||
>
|
||||
{analysisResult.analysis?.confidence || "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
{#if analysisResult.is_greenwashing}
|
||||
<div class="analysis-result">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Verdict:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.analysis?.verdict ||
|
||||
"N/A"}</span
|
||||
>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Confidence:</span>
|
||||
<span
|
||||
class="result-value badge {analysisResult
|
||||
.analysis?.confidence}"
|
||||
>
|
||||
{analysisResult.analysis?.confidence ||
|
||||
"unknown"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Severity:</span>
|
||||
<span
|
||||
@@ -192,29 +353,88 @@
|
||||
{analysisResult.analysis?.severity || "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="result-item full-width">
|
||||
<span class="result-label">Analysis:</span>
|
||||
<p class="result-text">
|
||||
{analysisResult.analysis?.reasoning ||
|
||||
"No details available"}
|
||||
</p>
|
||||
<div class="result-item full-width">
|
||||
<span class="result-label">Analysis:</span>
|
||||
<p class="result-text">
|
||||
{analysisResult.analysis?.reasoning ||
|
||||
"No details available"}
|
||||
</p>
|
||||
</div>
|
||||
{#if analysisResult.detected_brand && analysisResult.detected_brand !== "Unknown"}
|
||||
<div class="result-item">
|
||||
<span class="result-label">Detected Brand:</span
|
||||
>
|
||||
<span class="result-value"
|
||||
>{analysisResult.detected_brand}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if analysisResult.detected_brand && analysisResult.detected_brand !== "Unknown"}
|
||||
|
||||
<button
|
||||
class="back-btn"
|
||||
onclick={() => window.history.back()}
|
||||
>
|
||||
<iconify-icon icon="ri:arrow-left-line" width="20"
|
||||
></iconify-icon>
|
||||
Go Back
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Report Analyzed (No Greenwashing) -->
|
||||
<div class="icon-circle success">
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="60"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<h2 class="success-title">Report Analyzed</h2>
|
||||
|
||||
<div class="analysis-result">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Detected Brand:</span>
|
||||
<span class="result-label">Verdict:</span>
|
||||
<span class="result-value"
|
||||
>{analysisResult.detected_brand}</span
|
||||
>{analysisResult.analysis?.verdict ||
|
||||
"N/A"}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Confidence:</span>
|
||||
<span
|
||||
class="result-value badge {analysisResult
|
||||
.analysis?.confidence}"
|
||||
>
|
||||
{analysisResult.analysis?.confidence ||
|
||||
"unknown"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-item full-width">
|
||||
<span class="result-label">Analysis:</span>
|
||||
<p class="result-text">
|
||||
{analysisResult.analysis?.reasoning ||
|
||||
"No details available"}
|
||||
</p>
|
||||
</div>
|
||||
{#if analysisResult.detected_brand && analysisResult.detected_brand !== "Unknown"}
|
||||
<div class="result-item">
|
||||
<span class="result-label">Detected Brand:</span
|
||||
>
|
||||
<span class="result-value"
|
||||
>{analysisResult.detected_brand}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="back-btn" onclick={() => window.history.back()}>
|
||||
<iconify-icon icon="ri:arrow-left-line" width="20"
|
||||
></iconify-icon>
|
||||
Go Back
|
||||
</button>
|
||||
<button
|
||||
class="back-btn"
|
||||
onclick={() => window.history.back()}
|
||||
>
|
||||
<iconify-icon icon="ri:arrow-left-line" width="20"
|
||||
></iconify-icon>
|
||||
Go Back
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header-section">
|
||||
@@ -263,23 +483,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label" for="description"
|
||||
>Why is it misleading?</label
|
||||
>
|
||||
<div class="input-wrapper textarea-wrapper">
|
||||
<iconify-icon
|
||||
icon="ri:text-snippet-line"
|
||||
class="input-icon top-align"
|
||||
></iconify-icon>
|
||||
<textarea
|
||||
id="description"
|
||||
class="textarea"
|
||||
placeholder="Describe the claim and the reality..."
|
||||
bind:value={description}
|
||||
></textarea>
|
||||
{#if reportType === "product"}
|
||||
<div class="form-group">
|
||||
<label class="label" for="description"
|
||||
>Why is it misleading?</label
|
||||
>
|
||||
<div class="input-wrapper textarea-wrapper">
|
||||
<iconify-icon
|
||||
icon="ri:text-snippet-line"
|
||||
class="input-icon top-align"
|
||||
></iconify-icon>
|
||||
<textarea
|
||||
id="description"
|
||||
class="textarea"
|
||||
placeholder="Describe the claim and the reality..."
|
||||
bind:value={description}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-group">
|
||||
<span class="label">
|
||||
@@ -366,7 +588,11 @@
|
||||
width="24"
|
||||
class="pulse"
|
||||
></iconify-icon>
|
||||
<span>Analyzing Report</span>
|
||||
<span>
|
||||
{reportType === "product"
|
||||
? "Analyzing Report"
|
||||
: "Verifying & Uploading Report"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-steps">
|
||||
{#each progressSteps as step}
|
||||
@@ -417,9 +643,19 @@
|
||||
onclick={handleSubmit}
|
||||
type="button"
|
||||
>
|
||||
<iconify-icon icon="ri:shield-flash-line" width="20"
|
||||
></iconify-icon>
|
||||
Analyze for Greenwashing
|
||||
{#if reportType === "product"}
|
||||
<iconify-icon
|
||||
icon="ri:shield-flash-line"
|
||||
width="20"
|
||||
></iconify-icon>
|
||||
Analyze for Greenwashing
|
||||
{:else}
|
||||
<iconify-icon
|
||||
icon="ri:upload-cloud-2-line"
|
||||
width="20"
|
||||
></iconify-icon>
|
||||
Upload & Verify Report
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -443,7 +679,7 @@
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 80px 20px 40px;
|
||||
padding: 120px 20px 40px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -821,6 +1057,51 @@
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #22c55e;
|
||||
color: #052e16;
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #16a34a;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.topics-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.topic-tag {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #4ade80;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -11,9 +11,4 @@
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user