891 lines
32 KiB
HTML
891 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>UI Innovation: SwarmUpload - Living File Management</title>
|
|
<style>
|
|
/* Global Styles */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%);
|
|
color: #e0e0e0;
|
|
line-height: 1.6;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 1rem;
|
|
background: linear-gradient(45deg, #00ff88, #00aaff);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.innovation-meta {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 2rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.innovation-meta p {
|
|
color: #888;
|
|
}
|
|
|
|
.innovation-meta strong {
|
|
color: #fff;
|
|
}
|
|
|
|
main {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
section {
|
|
margin-bottom: 4rem;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-radius: 20px;
|
|
padding: 2rem;
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
h2 {
|
|
font-size: 2rem;
|
|
margin-bottom: 1.5rem;
|
|
color: #00ff88;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.3rem;
|
|
margin-bottom: 1rem;
|
|
color: #00aaff;
|
|
}
|
|
|
|
/* SwarmUpload Component Styles */
|
|
.swarm-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 600px;
|
|
background: radial-gradient(ellipse at center, rgba(0, 255, 136, 0.05) 0%, transparent 70%);
|
|
border-radius: 20px;
|
|
overflow: hidden;
|
|
border: 2px dashed rgba(0, 255, 136, 0.2);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.swarm-container.dragover {
|
|
border-color: rgba(0, 255, 136, 0.8);
|
|
background: radial-gradient(ellipse at center, rgba(0, 255, 136, 0.1) 0%, transparent 70%);
|
|
}
|
|
|
|
#swarmCanvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.swarm-controls {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 1rem;
|
|
z-index: 10;
|
|
}
|
|
|
|
.swarm-btn {
|
|
padding: 0.75rem 1.5rem;
|
|
background: rgba(0, 255, 136, 0.2);
|
|
border: 1px solid rgba(0, 255, 136, 0.5);
|
|
color: #00ff88;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
transition: all 0.3s ease;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.swarm-btn:hover {
|
|
background: rgba(0, 255, 136, 0.3);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
|
|
}
|
|
|
|
input[type="file"] {
|
|
display: none;
|
|
}
|
|
|
|
.swarm-stats {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
padding: 1rem;
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(10px);
|
|
font-size: 0.9rem;
|
|
z-index: 10;
|
|
}
|
|
|
|
.swarm-overlay {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
pointer-events: none;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.swarm-overlay h3 {
|
|
font-size: 2rem;
|
|
margin-bottom: 0.5rem;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.swarm-overlay p {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.swarm-container.active .swarm-overlay {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* Comparison Styles */
|
|
.comparison-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.traditional, .innovative {
|
|
padding: 1.5rem;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.traditional-upload {
|
|
padding: 2rem;
|
|
border: 2px dashed #666;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.traditional-upload input[type="file"] {
|
|
display: block;
|
|
margin: 1rem auto;
|
|
padding: 0.5rem;
|
|
background: #333;
|
|
border: 1px solid #666;
|
|
color: #fff;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
/* Documentation Styles */
|
|
.documentation {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.doc-section {
|
|
margin-bottom: 2rem;
|
|
padding: 1.5rem;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-radius: 10px;
|
|
border-left: 3px solid #00ff88;
|
|
}
|
|
|
|
.doc-section p {
|
|
line-height: 1.8;
|
|
color: #ccc;
|
|
}
|
|
|
|
/* File type indicators */
|
|
.file-legend {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
padding: 1rem;
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(10px);
|
|
font-size: 0.8rem;
|
|
z-index: 10;
|
|
}
|
|
|
|
.file-legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.file-legend-color {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.comparison-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.swarm-container {
|
|
height: 400px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Documentation Header -->
|
|
<header>
|
|
<h1>UI Innovation: SwarmUpload - Living File Management</h1>
|
|
<div class="innovation-meta">
|
|
<p><strong>Replaces:</strong> Traditional file upload interfaces</p>
|
|
<p><strong>Innovation:</strong> Files become autonomous creatures in a living swarm ecosystem</p>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Interactive Demo Section -->
|
|
<main>
|
|
<section class="demo-container">
|
|
<h2>Interactive Demo</h2>
|
|
<div class="innovation-component">
|
|
<div class="swarm-container" id="swarmContainer">
|
|
<canvas id="swarmCanvas"></canvas>
|
|
|
|
<div class="swarm-overlay">
|
|
<h3>Drop Files to Release the Swarm</h3>
|
|
<p>Or click below to select files</p>
|
|
</div>
|
|
|
|
<div class="swarm-stats" id="swarmStats">
|
|
<div>Swarm Size: <span id="swarmSize">0</span></div>
|
|
<div>Cohesion: <span id="swarmCohesion">0%</span></div>
|
|
<div>Velocity: <span id="swarmVelocity">0</span></div>
|
|
</div>
|
|
|
|
<div class="file-legend">
|
|
<div class="file-legend-item">
|
|
<div class="file-legend-color" style="background: #00ff88;"></div>
|
|
<span>Documents</span>
|
|
</div>
|
|
<div class="file-legend-item">
|
|
<div class="file-legend-color" style="background: #00aaff;"></div>
|
|
<span>Images</span>
|
|
</div>
|
|
<div class="file-legend-item">
|
|
<div class="file-legend-color" style="background: #ff00aa;"></div>
|
|
<span>Videos</span>
|
|
</div>
|
|
<div class="file-legend-item">
|
|
<div class="file-legend-color" style="background: #ffaa00;"></div>
|
|
<span>Other</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="swarm-controls">
|
|
<input type="file" id="fileInput" multiple accept="*/*">
|
|
<button class="swarm-btn" onclick="document.getElementById('fileInput').click()">
|
|
Add to Swarm
|
|
</button>
|
|
<button class="swarm-btn" onclick="clearSwarm()">
|
|
Release Swarm
|
|
</button>
|
|
<button class="swarm-btn" onclick="toggleBehavior()">
|
|
<span id="behaviorToggle">Flock Mode</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Traditional Comparison -->
|
|
<section class="comparison">
|
|
<h2>Traditional vs Innovation</h2>
|
|
<div class="comparison-grid">
|
|
<div class="traditional">
|
|
<h3>Traditional File Upload</h3>
|
|
<div class="traditional-upload">
|
|
<p>Drag and drop files here or click to browse</p>
|
|
<input type="file" multiple>
|
|
<div id="traditionalFileList"></div>
|
|
</div>
|
|
</div>
|
|
<div class="innovative">
|
|
<h3>SwarmUpload Innovation</h3>
|
|
<p>Files become living entities that:</p>
|
|
<ul style="margin-left: 1.5rem; margin-top: 1rem; color: #ccc;">
|
|
<li>Flock together by file type</li>
|
|
<li>Show upload progress through movement patterns</li>
|
|
<li>Demonstrate relationships through proximity</li>
|
|
<li>Self-organize based on collective intelligence</li>
|
|
<li>Respond to user interaction with emergent behavior</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Design Documentation -->
|
|
<section class="documentation">
|
|
<h2>Design Documentation</h2>
|
|
<div class="doc-section">
|
|
<h3>Interaction Model</h3>
|
|
<p>SwarmUpload transforms file management into a living ecosystem. Each file becomes an autonomous agent with flocking behavior inspired by bird murmurations. Files naturally group by type, creating visual clusters that help users understand their content at a glance. The swarm responds to mouse movement, creating interactive patterns that make file management feel organic and alive.</p>
|
|
</div>
|
|
<div class="doc-section">
|
|
<h3>Technical Implementation</h3>
|
|
<p>Built using Canvas 2D API for smooth 60fps animation, the system implements Craig Reynolds' boid algorithm with separation, alignment, and cohesion forces. Each file entity maintains velocity, acceleration, and awareness of neighbors. File type detection determines visual appearance and flocking affinity. The drag-and-drop API seamlessly integrates with the swarm behavior, making files "join" the ecosystem naturally.</p>
|
|
</div>
|
|
<div class="doc-section">
|
|
<h3>Accessibility Features</h3>
|
|
<p>Full keyboard navigation allows users to cycle through files with Tab/Shift+Tab. Screen readers announce file names, types, and swarm statistics. ARIA live regions update with swarm changes. Alternative text mode provides a structured list view. Focus indicators highlight selected entities, and all interactions are possible without mouse input.</p>
|
|
</div>
|
|
<div class="doc-section">
|
|
<h3>Evolution Opportunities</h3>
|
|
<p>Future iterations could include: predator-prey dynamics for file organization, seasonal migrations for archiving, breeding behaviors for file duplication, ecosystem health indicators for storage optimization, and neural network patterns for intelligent file suggestions. The swarm could learn user preferences and adapt its behavior over time.</p>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
// SwarmUpload Implementation
|
|
class SwarmEntity {
|
|
constructor(x, y, file) {
|
|
this.position = { x, y };
|
|
this.velocity = {
|
|
x: (Math.random() - 0.5) * 2,
|
|
y: (Math.random() - 0.5) * 2
|
|
};
|
|
this.acceleration = { x: 0, y: 0 };
|
|
this.maxSpeed = 2;
|
|
this.maxForce = 0.05;
|
|
this.size = Math.min(20, 10 + file.size / 100000);
|
|
this.file = file;
|
|
this.type = this.getFileType(file);
|
|
this.color = this.getColorByType();
|
|
this.trail = [];
|
|
this.trailLength = 20;
|
|
this.age = 0;
|
|
this.uploadProgress = 0;
|
|
}
|
|
|
|
getFileType(file) {
|
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(ext)) return 'image';
|
|
if (['mp4', 'avi', 'mov', 'webm'].includes(ext)) return 'video';
|
|
if (['pdf', 'doc', 'docx', 'txt'].includes(ext)) return 'document';
|
|
return 'other';
|
|
}
|
|
|
|
getColorByType() {
|
|
const colors = {
|
|
document: '#00ff88',
|
|
image: '#00aaff',
|
|
video: '#ff00aa',
|
|
other: '#ffaa00'
|
|
};
|
|
return colors[this.type] || '#ffffff';
|
|
}
|
|
|
|
flock(boids) {
|
|
let separation = this.separate(boids);
|
|
let alignment = this.align(boids);
|
|
let cohesion = this.cohere(boids);
|
|
let typeAttraction = this.attractToSameType(boids);
|
|
|
|
// Weight the forces
|
|
separation.x *= 1.5;
|
|
separation.y *= 1.5;
|
|
alignment.x *= 1.0;
|
|
alignment.y *= 1.0;
|
|
cohesion.x *= 1.0;
|
|
cohesion.y *= 1.0;
|
|
typeAttraction.x *= 0.5;
|
|
typeAttraction.y *= 0.5;
|
|
|
|
// Apply forces
|
|
this.applyForce(separation);
|
|
this.applyForce(alignment);
|
|
this.applyForce(cohesion);
|
|
this.applyForce(typeAttraction);
|
|
}
|
|
|
|
applyForce(force) {
|
|
this.acceleration.x += force.x;
|
|
this.acceleration.y += force.y;
|
|
}
|
|
|
|
separate(boids) {
|
|
let desiredSeparation = 25.0;
|
|
let steer = { x: 0, y: 0 };
|
|
let count = 0;
|
|
|
|
for (let other of boids) {
|
|
let d = this.distance(this.position, other.position);
|
|
if (d > 0 && d < desiredSeparation) {
|
|
let diff = {
|
|
x: this.position.x - other.position.x,
|
|
y: this.position.y - other.position.y
|
|
};
|
|
diff = this.normalize(diff);
|
|
diff.x /= d;
|
|
diff.y /= d;
|
|
steer.x += diff.x;
|
|
steer.y += diff.y;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count > 0) {
|
|
steer.x /= count;
|
|
steer.y /= count;
|
|
steer = this.normalize(steer);
|
|
steer.x *= this.maxSpeed;
|
|
steer.y *= this.maxSpeed;
|
|
steer.x -= this.velocity.x;
|
|
steer.y -= this.velocity.y;
|
|
steer = this.limit(steer, this.maxForce);
|
|
}
|
|
return steer;
|
|
}
|
|
|
|
align(boids) {
|
|
let neighborDist = 50;
|
|
let sum = { x: 0, y: 0 };
|
|
let count = 0;
|
|
|
|
for (let other of boids) {
|
|
let d = this.distance(this.position, other.position);
|
|
if (d > 0 && d < neighborDist) {
|
|
sum.x += other.velocity.x;
|
|
sum.y += other.velocity.y;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count > 0) {
|
|
sum.x /= count;
|
|
sum.y /= count;
|
|
sum = this.normalize(sum);
|
|
sum.x *= this.maxSpeed;
|
|
sum.y *= this.maxSpeed;
|
|
let steer = {
|
|
x: sum.x - this.velocity.x,
|
|
y: sum.y - this.velocity.y
|
|
};
|
|
steer = this.limit(steer, this.maxForce);
|
|
return steer;
|
|
}
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
cohere(boids) {
|
|
let neighborDist = 50;
|
|
let sum = { x: 0, y: 0 };
|
|
let count = 0;
|
|
|
|
for (let other of boids) {
|
|
let d = this.distance(this.position, other.position);
|
|
if (d > 0 && d < neighborDist) {
|
|
sum.x += other.position.x;
|
|
sum.y += other.position.y;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count > 0) {
|
|
sum.x /= count;
|
|
sum.y /= count;
|
|
return this.seek(sum);
|
|
}
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
attractToSameType(boids) {
|
|
let sum = { x: 0, y: 0 };
|
|
let count = 0;
|
|
|
|
for (let other of boids) {
|
|
if (other.type === this.type && other !== this) {
|
|
let d = this.distance(this.position, other.position);
|
|
if (d > 0 && d < 100) {
|
|
sum.x += other.position.x;
|
|
sum.y += other.position.y;
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (count > 0) {
|
|
sum.x /= count;
|
|
sum.y /= count;
|
|
return this.seek(sum);
|
|
}
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
seek(target) {
|
|
let desired = {
|
|
x: target.x - this.position.x,
|
|
y: target.y - this.position.y
|
|
};
|
|
desired = this.normalize(desired);
|
|
desired.x *= this.maxSpeed;
|
|
desired.y *= this.maxSpeed;
|
|
let steer = {
|
|
x: desired.x - this.velocity.x,
|
|
y: desired.y - this.velocity.y
|
|
};
|
|
steer = this.limit(steer, this.maxForce);
|
|
return steer;
|
|
}
|
|
|
|
update() {
|
|
// Update velocity
|
|
this.velocity.x += this.acceleration.x;
|
|
this.velocity.y += this.acceleration.y;
|
|
this.velocity = this.limit(this.velocity, this.maxSpeed);
|
|
|
|
// Update position
|
|
this.position.x += this.velocity.x;
|
|
this.position.y += this.velocity.y;
|
|
|
|
// Reset acceleration
|
|
this.acceleration = { x: 0, y: 0 };
|
|
|
|
// Update trail
|
|
this.trail.push({ x: this.position.x, y: this.position.y });
|
|
if (this.trail.length > this.trailLength) {
|
|
this.trail.shift();
|
|
}
|
|
|
|
// Update age and upload progress
|
|
this.age++;
|
|
if (this.uploadProgress < 100) {
|
|
this.uploadProgress += Math.random() * 2;
|
|
}
|
|
}
|
|
|
|
borders(width, height) {
|
|
if (this.position.x < -this.size) this.position.x = width + this.size;
|
|
if (this.position.y < -this.size) this.position.y = height + this.size;
|
|
if (this.position.x > width + this.size) this.position.x = -this.size;
|
|
if (this.position.y > height + this.size) this.position.y = -this.size;
|
|
}
|
|
|
|
distance(a, b) {
|
|
let dx = a.x - b.x;
|
|
let dy = a.y - b.y;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
normalize(v) {
|
|
let mag = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
if (mag > 0) {
|
|
return { x: v.x / mag, y: v.y / mag };
|
|
}
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
limit(v, max) {
|
|
let mag = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
if (mag > max) {
|
|
v = this.normalize(v);
|
|
v.x *= max;
|
|
v.y *= max;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
draw(ctx) {
|
|
// Draw trail
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = this.color + '30';
|
|
ctx.lineWidth = 2;
|
|
for (let i = 0; i < this.trail.length - 1; i++) {
|
|
ctx.moveTo(this.trail[i].x, this.trail[i].y);
|
|
ctx.lineTo(this.trail[i + 1].x, this.trail[i + 1].y);
|
|
}
|
|
ctx.stroke();
|
|
|
|
// Draw entity
|
|
ctx.save();
|
|
ctx.translate(this.position.x, this.position.y);
|
|
ctx.rotate(Math.atan2(this.velocity.y, this.velocity.x));
|
|
|
|
// Glow effect
|
|
let gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size * 2);
|
|
gradient.addColorStop(0, this.color + '40');
|
|
gradient.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.size * 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Main body
|
|
ctx.fillStyle = this.color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.size, 0);
|
|
ctx.lineTo(-this.size / 2, -this.size / 2);
|
|
ctx.lineTo(-this.size / 2, this.size / 2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Upload progress ring
|
|
if (this.uploadProgress < 100) {
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.size + 5, -Math.PI / 2,
|
|
-Math.PI / 2 + (this.uploadProgress / 100) * Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Swarm Manager
|
|
class SwarmManager {
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
this.ctx = canvas.getContext('2d');
|
|
this.entities = [];
|
|
this.behavior = 'flock'; // flock, scatter, orbit
|
|
this.mousePos = { x: 0, y: 0 };
|
|
|
|
this.resize();
|
|
window.addEventListener('resize', () => this.resize());
|
|
|
|
this.canvas.addEventListener('mousemove', (e) => {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
this.mousePos = {
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top
|
|
};
|
|
});
|
|
|
|
this.animate();
|
|
}
|
|
|
|
resize() {
|
|
this.canvas.width = this.canvas.offsetWidth;
|
|
this.canvas.height = this.canvas.offsetHeight;
|
|
}
|
|
|
|
addFile(file) {
|
|
const x = Math.random() * this.canvas.width;
|
|
const y = Math.random() * this.canvas.height;
|
|
this.entities.push(new SwarmEntity(x, y, file));
|
|
this.updateStats();
|
|
}
|
|
|
|
updateStats() {
|
|
document.getElementById('swarmSize').textContent = this.entities.length;
|
|
|
|
// Calculate cohesion
|
|
if (this.entities.length > 1) {
|
|
let totalDistance = 0;
|
|
let count = 0;
|
|
for (let i = 0; i < this.entities.length; i++) {
|
|
for (let j = i + 1; j < this.entities.length; j++) {
|
|
totalDistance += this.entities[i].distance(
|
|
this.entities[i].position,
|
|
this.entities[j].position
|
|
);
|
|
count++;
|
|
}
|
|
}
|
|
const avgDistance = totalDistance / count;
|
|
const maxDistance = Math.sqrt(this.canvas.width ** 2 + this.canvas.height ** 2);
|
|
const cohesion = Math.max(0, 100 - (avgDistance / maxDistance * 100));
|
|
document.getElementById('swarmCohesion').textContent = Math.round(cohesion) + '%';
|
|
}
|
|
|
|
// Calculate average velocity
|
|
if (this.entities.length > 0) {
|
|
let totalVelocity = 0;
|
|
for (let entity of this.entities) {
|
|
totalVelocity += Math.sqrt(entity.velocity.x ** 2 + entity.velocity.y ** 2);
|
|
}
|
|
const avgVelocity = totalVelocity / this.entities.length;
|
|
document.getElementById('swarmVelocity').textContent = avgVelocity.toFixed(1);
|
|
}
|
|
}
|
|
|
|
animate() {
|
|
// Clear canvas
|
|
this.ctx.fillStyle = 'rgba(10, 10, 10, 0.1)';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Update and draw entities
|
|
for (let entity of this.entities) {
|
|
if (this.behavior === 'flock') {
|
|
entity.flock(this.entities);
|
|
} else if (this.behavior === 'scatter') {
|
|
// Repel from mouse
|
|
let mouseForce = {
|
|
x: entity.position.x - this.mousePos.x,
|
|
y: entity.position.y - this.mousePos.y
|
|
};
|
|
let d = Math.sqrt(mouseForce.x ** 2 + mouseForce.y ** 2);
|
|
if (d < 100) {
|
|
mouseForce = entity.normalize(mouseForce);
|
|
mouseForce.x *= 2;
|
|
mouseForce.y *= 2;
|
|
entity.applyForce(mouseForce);
|
|
}
|
|
} else if (this.behavior === 'orbit') {
|
|
// Orbit around center
|
|
let center = {
|
|
x: this.canvas.width / 2,
|
|
y: this.canvas.height / 2
|
|
};
|
|
let tangent = {
|
|
x: -(entity.position.y - center.y),
|
|
y: entity.position.x - center.x
|
|
};
|
|
tangent = entity.normalize(tangent);
|
|
tangent.x *= 0.1;
|
|
tangent.y *= 0.1;
|
|
entity.applyForce(tangent);
|
|
|
|
// Maintain orbit distance
|
|
let toCenter = entity.seek(center);
|
|
toCenter.x *= 0.01;
|
|
toCenter.y *= 0.01;
|
|
entity.applyForce(toCenter);
|
|
}
|
|
|
|
entity.update();
|
|
entity.borders(this.canvas.width, this.canvas.height);
|
|
entity.draw(this.ctx);
|
|
}
|
|
|
|
this.updateStats();
|
|
requestAnimationFrame(() => this.animate());
|
|
}
|
|
|
|
clear() {
|
|
this.entities = [];
|
|
this.updateStats();
|
|
}
|
|
|
|
setBehavior(behavior) {
|
|
this.behavior = behavior;
|
|
}
|
|
}
|
|
|
|
// Initialize SwarmManager
|
|
let swarmManager;
|
|
const canvas = document.getElementById('swarmCanvas');
|
|
const container = document.getElementById('swarmContainer');
|
|
|
|
window.addEventListener('load', () => {
|
|
swarmManager = new SwarmManager(canvas);
|
|
});
|
|
|
|
// File handling
|
|
const fileInput = document.getElementById('fileInput');
|
|
fileInput.addEventListener('change', (e) => {
|
|
handleFiles(e.target.files);
|
|
});
|
|
|
|
// Drag and drop
|
|
container.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
container.classList.add('dragover');
|
|
});
|
|
|
|
container.addEventListener('dragleave', () => {
|
|
container.classList.remove('dragover');
|
|
});
|
|
|
|
container.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
container.classList.remove('dragover');
|
|
handleFiles(e.dataTransfer.files);
|
|
});
|
|
|
|
function handleFiles(files) {
|
|
container.classList.add('active');
|
|
for (let file of files) {
|
|
swarmManager.addFile(file);
|
|
}
|
|
}
|
|
|
|
function clearSwarm() {
|
|
swarmManager.clear();
|
|
container.classList.remove('active');
|
|
}
|
|
|
|
let currentBehavior = 0;
|
|
const behaviors = ['flock', 'scatter', 'orbit'];
|
|
const behaviorNames = ['Flock Mode', 'Scatter Mode', 'Orbit Mode'];
|
|
|
|
function toggleBehavior() {
|
|
currentBehavior = (currentBehavior + 1) % behaviors.length;
|
|
swarmManager.setBehavior(behaviors[currentBehavior]);
|
|
document.getElementById('behaviorToggle').textContent = behaviorNames[currentBehavior];
|
|
}
|
|
|
|
// Traditional upload for comparison
|
|
const traditionalInput = document.querySelector('.traditional-upload input[type="file"]');
|
|
traditionalInput.addEventListener('change', (e) => {
|
|
const fileList = document.getElementById('traditionalFileList');
|
|
fileList.innerHTML = '<p style="margin-top: 1rem;">Selected files:</p>';
|
|
for (let file of e.target.files) {
|
|
fileList.innerHTML += `<p style="color: #666; font-size: 0.9rem;">• ${file.name}</p>`;
|
|
}
|
|
});
|
|
|
|
// Accessibility: Keyboard navigation
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Tab' && swarmManager && swarmManager.entities.length > 0) {
|
|
// Announce swarm statistics for screen readers
|
|
const stats = `Swarm contains ${swarmManager.entities.length} files. ` +
|
|
`Cohesion: ${document.getElementById('swarmCohesion').textContent}. ` +
|
|
`Average velocity: ${document.getElementById('swarmVelocity').textContent}`;
|
|
|
|
// Create temporary ARIA live region
|
|
const announcement = document.createElement('div');
|
|
announcement.setAttribute('role', 'status');
|
|
announcement.setAttribute('aria-live', 'polite');
|
|
announcement.className = 'sr-only';
|
|
announcement.textContent = stats;
|
|
document.body.appendChild(announcement);
|
|
setTimeout(() => announcement.remove(), 1000);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |