feat: Add website v1
Not final: Needs more file icons clean up some text
This commit is contained in:
19
website/src/app/components/ad-slot/ad-slot.component.ts
Normal file
19
website/src/app/components/ad-slot/ad-slot.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AdService } from '../../services/ad.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ad-slot',
|
||||
standalone: true,
|
||||
template: `
|
||||
@if (adService.adsEnabled()) {
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="rounded-lg border border-dashed border-border/50 bg-card/30 min-h-[90px] flex items-center justify-center text-xs text-muted-foreground/50">
|
||||
Advertisement
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class AdSlotComponent {
|
||||
readonly adService = inject(AdService);
|
||||
}
|
||||
70
website/src/app/components/footer/footer.component.ts
Normal file
70
website/src/app/components/footer/footer.component.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
template: `
|
||||
<footer class="border-t border-border/30 bg-background/80 backdrop-blur-sm">
|
||||
<div class="container mx-auto px-6 py-16">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||
<!-- Brand -->
|
||||
<div class="md:col-span-1">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center font-bold text-white text-sm">T</div>
|
||||
<span class="text-lg font-bold text-foreground">Toju</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Free, open-source, peer-to-peer communication. Built by people who believe privacy is a right, not a premium feature.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Product -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Product</h4>
|
||||
<ul class="space-y-3">
|
||||
<li><a routerLink="/downloads" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a></li>
|
||||
<li><a href="https://web.toju.app/" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Web Version</a></li>
|
||||
<li><a routerLink="/what-is-toju" class="text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Community -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Community</h4>
|
||||
<ul class="space-y-3">
|
||||
<li><a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Source Code</a></li>
|
||||
<li><a href="https://github.com/Myxelium" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">GitHub</a></li>
|
||||
<li><a href="https://buymeacoffee.com/myxelium" target="_blank" rel="noopener" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Support Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Values -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-4 uppercase tracking-wider">Values</h4>
|
||||
<ul class="space-y-3">
|
||||
<li><a routerLink="/philosophy" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a></li>
|
||||
<li><span class="text-sm text-muted-foreground">100% Free Forever</span></li>
|
||||
<li><span class="text-sm text-muted-foreground">Open Source</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 pt-8 border-t border-border/30 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
© {{ currentYear }} Myxelium. Toju is open-source software.
|
||||
</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="https://git.azaaxin.com/myxelium/Toju" target="_blank" rel="noopener" class="text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
`
|
||||
})
|
||||
export class FooterComponent {
|
||||
readonly currentYear = new Date().getFullYear();
|
||||
}
|
||||
102
website/src/app/components/header/header.component.ts
Normal file
102
website/src/app/components/header/header.component.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
HostListener,
|
||||
PLATFORM_ID
|
||||
} from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
standalone: true,
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
template: `
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
|
||||
[class]="scrolled() ? 'glass shadow-lg shadow-black/20' : 'bg-transparent'"
|
||||
>
|
||||
<nav class="container mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<a routerLink="/" class="flex items-center gap-3 group">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center font-bold text-white text-lg shadow-lg shadow-purple-500/25 group-hover:shadow-purple-500/40 transition-shadow">
|
||||
T
|
||||
</div>
|
||||
<span class="text-xl font-bold text-foreground">Toju</span>
|
||||
<span class="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/30 uppercase tracking-wider">Beta</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
<a routerLink="/" routerLinkActive="text-primary" [routerLinkActiveOptions]="{exact: true}" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
|
||||
<a routerLink="/what-is-toju" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a>
|
||||
<a routerLink="/downloads" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a>
|
||||
<a routerLink="/philosophy" routerLinkActive="text-primary" class="text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a>
|
||||
</div>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<a
|
||||
href="https://buymeacoffee.com/myxelium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-muted-foreground hover:text-yellow-400 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.062 2.014.13l.04.005c.394.04.781.098 1.172.166.063.011.121.029.182.044.088.03.18.043.252.087a.556.556 0 01.272.476c.014.09.042.217.068.343.082.397.162.79.24 1.184.048.26.092.52.122.782.017.163.047.414-.076.556-.163.178-.49.132-.702.096a48.146 48.146 0 00-1.553-.22 30.38 30.38 0 00-3.346-.234c-1.108 0-2.217.066-3.315.2a25.855 25.855 0 00-1.578.244c-.156.03-.313.062-.469.097-.288.065-.564.195-.754.441-.131.17-.18.383-.134.596.046.213.167.396.34.525.294.215.626.358.976.454 1.015.285 2.087.38 3.136.397 1.21.018 2.42-.04 3.618-.177 1.004-.114 2-.3 2.95-.593.082-.025.165-.05.246-.078.224-.073.458-.142.676-.233.378-.157.727-.397.856-.795a1.38 1.38 0 00.048-.353v-.015c0-.049-.01-.096-.014-.144l-.018-.121c-.028-.179-.055-.362-.1-.539l-.025-.115-.038-.186c-.022-.11-.044-.22-.07-.327l-.061-.283c-.02-.096-.042-.19-.067-.284-.02-.074-.037-.15-.058-.224a2.98 2.98 0 00-.09-.3c-.034-.095-.078-.225-.148-.308zm-11.8 6.56a.612.612 0 00-.35.177.614.614 0 00-.17.343l-.14.697-.147.726c-.012.065-.018.13-.026.194l-.13.653c-.012.065-.018.13-.026.195l-.13.652c-.024.119-.042.24-.07.358-.018.073-.037.147-.05.222l-.087.437c-.024.12-.042.24-.07.359-.018.073-.037.146-.05.221l-.088.437c-.011.058-.022.117-.03.176-.01.06-.014.119-.025.178a.474.474 0 00.147.422c.105.1.24.153.378.154l1.06.005c.037 0 .073-.003.11-.009l.053-.009c.151-.03.293-.109.364-.255.043-.09.058-.186.065-.282v-.003l.05-.259.052-.259.023-.117c.01-.051.019-.103.03-.153l.005-.025.026-.132.027-.131.026-.131.052-.259.052-.259.013-.064.013-.064.024-.12.024-.121c.012-.062.007-.127-.003-.189a.57.57 0 00-.044-.15.538.538 0 00-.094-.14.596.596 0 00-.23-.155c-.07-.032-.147-.052-.226-.057-.118-.008-.236-.008-.355-.008h-.007c-.154 0-.308 0-.462.003-.107.002-.216.002-.323.006zm13.7-2.974a.53.53 0 01.128.378c-.004.064-.02.128-.048.186a.684.684 0 01-.097.148l-.012.014-.016.016a.585.585 0 01-.13.098.612.612 0 01-.21.075 1.378 1.378 0 01-.26.024h-.037c-.086 0-.132.093-.094.161a.3.3 0 01.032.063c.018.045.018.093.009.14-.011.059-.048.11-.09.154a.332.332 0 01-.145.1.375.375 0 01-.173.03.505.505 0 01-.152-.036.43.43 0 01-.123-.082.456.456 0 01-.085-.11.435.435 0 01-.044-.138 1.01 1.01 0 01-.017-.134.66.66 0 01.005-.133c.008-.053.032-.1.065-.143.035-.044.078-.08.126-.107a.498.498 0 01.163-.059c.05-.007.1-.007.15-.002h.015a.08.08 0 00.076-.048.083.083 0 00-.023-.096.568.568 0 01-.123-.15.516.516 0 01-.056-.185.582.582 0 01.013-.196.592.592 0 01.075-.176.538.538 0 01.198-.182.567.567 0 01.247-.07c.08-.003.16.011.232.044a.56.56 0 01.187.14z"/></svg>
|
||||
Support Us
|
||||
</a>
|
||||
<a
|
||||
href="https://web.toju.app/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium hover:from-purple-500 hover:to-violet-500 transition-all shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40"
|
||||
>
|
||||
Use Web Version
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile hamburger -->
|
||||
<button
|
||||
class="md:hidden text-foreground p-2"
|
||||
(click)="mobileOpen.set(!mobileOpen())"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@if (mobileOpen()) {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
} @else {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
}
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile nav -->
|
||||
@if (mobileOpen()) {
|
||||
<div class="md:hidden glass border-t border-border/30 px-6 py-4 space-y-4" (click)="mobileOpen.set(false)">
|
||||
<a routerLink="/" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Home</a>
|
||||
<a routerLink="/what-is-toju" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">What is Toju?</a>
|
||||
<a routerLink="/downloads" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Downloads</a>
|
||||
<a routerLink="/philosophy" class="block text-sm text-muted-foreground hover:text-foreground transition-colors">Our Philosophy</a>
|
||||
<hr class="border-border/30">
|
||||
<a href="https://buymeacoffee.com/myxelium" target="_blank" rel="noopener noreferrer" class="block text-sm text-muted-foreground hover:text-yellow-400 transition-colors">Support Us</a>
|
||||
<a href="https://web.toju.app/" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-violet-600 text-white text-sm font-medium">Use Web Version</a>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
`
|
||||
})
|
||||
export class HeaderComponent {
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
readonly scrolled = signal(false);
|
||||
readonly mobileOpen = signal(false);
|
||||
|
||||
@HostListener('window:scroll')
|
||||
onScroll(): void {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.scrolled.set(window.scrollY > 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
177
website/src/app/components/particle-bg/particle-bg.component.ts
Normal file
177
website/src/app/components/particle-bg/particle-bg.component.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
PLATFORM_ID,
|
||||
ElementRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particle-bg',
|
||||
standalone: true,
|
||||
template: '<canvas #canvas class="fixed inset-0 pointer-events-none z-0" style="pointer-events: all;"></canvas>',
|
||||
styles: [':host { display: block; position: fixed; inset: 0; z-index: 0; pointer-events: none; } canvas { pointer-events: all; }']
|
||||
})
|
||||
export class ParticleBgComponent implements OnInit, OnDestroy {
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
@ViewChild('canvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
private particles: Particle[] = [];
|
||||
private mouse = { x: -1000, y: -1000 };
|
||||
private animId = 0;
|
||||
private resizeHandler = () => this.resize();
|
||||
private mouseMoveHandler = (e: MouseEvent) => {
|
||||
this.mouse.x = e.clientX;
|
||||
this.mouse.y = e.clientY;
|
||||
};
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!isPlatformBrowser(this.platformId))
|
||||
return;
|
||||
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.resize();
|
||||
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
window.addEventListener('mousemove', this.mouseMoveHandler);
|
||||
|
||||
this.initParticles();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (!isPlatformBrowser(this.platformId))
|
||||
return;
|
||||
|
||||
cancelAnimationFrame(this.animId);
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
window.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||
}
|
||||
|
||||
private resize(): void {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
private initParticles(): void {
|
||||
const count = Math.min(80, Math.floor((window.innerWidth * window.innerHeight) / 15000));
|
||||
|
||||
this.particles = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * window.innerWidth,
|
||||
y: Math.random() * window.innerHeight,
|
||||
vx: (Math.random() - 0.5) * 0.4,
|
||||
vy: (Math.random() - 0.5) * 0.4,
|
||||
radius: Math.random() * 2 + 0.5,
|
||||
opacity: Math.random() * 0.5 + 0.1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private animate(): void {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
|
||||
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (const p of this.particles) {
|
||||
// Mouse interaction - repel
|
||||
const dx = p.x - this.mouse.x;
|
||||
const dy = p.y - this.mouse.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 150) {
|
||||
const force = (150 - dist) / 150;
|
||||
|
||||
p.vx += (dx / dist) * force * 0.3;
|
||||
p.vy += (dy / dist) * force * 0.3;
|
||||
}
|
||||
|
||||
// Dampen velocity
|
||||
p.vx *= 0.98;
|
||||
p.vy *= 0.98;
|
||||
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
|
||||
// Wrap
|
||||
if (p.x < 0)
|
||||
p.x = canvas.width;
|
||||
|
||||
if (p.x > canvas.width)
|
||||
p.x = 0;
|
||||
|
||||
if (p.y < 0)
|
||||
p.y = canvas.height;
|
||||
|
||||
if (p.y > canvas.height)
|
||||
p.y = 0;
|
||||
|
||||
// Draw particle
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = `rgba(139, 92, 246, ${p.opacity})`;
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
// Draw connections
|
||||
for (let i = 0; i < this.particles.length; i++) {
|
||||
for (let j = i + 1; j < this.particles.length; j++) {
|
||||
const a = this.particles[i];
|
||||
const b = this.particles[j];
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 120) {
|
||||
const opacity = (1 - dist / 120) * 0.15;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(a.x, a.y);
|
||||
this.ctx.lineTo(b.x, b.y);
|
||||
this.ctx.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
|
||||
this.ctx.lineWidth = 0.5;
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw connections to mouse
|
||||
for (const p of this.particles) {
|
||||
const dx = p.x - this.mouse.x;
|
||||
const dy = p.y - this.mouse.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 200) {
|
||||
const opacity = (1 - dist / 200) * 0.25;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(p.x, p.y);
|
||||
this.ctx.lineTo(this.mouse.x, this.mouse.y);
|
||||
this.ctx.strokeStyle = `rgba(139, 92, 246, ${opacity})`;
|
||||
this.ctx.lineWidth = 0.7;
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
this.animId = requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
}
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
radius: number;
|
||||
opacity: number;
|
||||
}
|
||||
Reference in New Issue
Block a user