-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcloth.js
More file actions
458 lines (387 loc) · 13.4 KB
/
cloth.js
File metadata and controls
458 lines (387 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
// Cloth simulation parameters
const GRID_SIZE = 20; // Number of points in each dimension
const REST_LENGTH = 20; // Rest length of springs
const STIFFNESS = 0.8; // Higher stiffness for cloth-like behavior
const DAMPING = 0.97; // Higher damping for less oscillation
const GRAVITY = 0.2; // Gravity force
const MOUSE_CUTOFF = 30; // Distance for point selection
const CUT_RADIUS = 20; // Radius for cutting when clicking
const MAX_TENSION = 35; // Maximum distance before automatic tearing
const REPULSION_RADIUS = REST_LENGTH * 0.75; // Radius for particle repulsion
const REPULSION_STRENGTH = 0.5; // Strength of repulsion
// Array to hold all points in the cloth
let points = [];
let sticks = [];
let isCutting = false;
let selectedPoint = null; // Track the selected point for dragging
// Create Point class
class Point {
constructor(x, y, pinned = false) {
this.pos = createVector(x, y);
this.prevPos = createVector(x, y);
this.vel = createVector(0, 0);
this.acc = createVector(0, 0);
this.pinned = pinned;
this.selected = false;
this.mass = 1.0; // Mass affects how points respond to forces
}
// Apply forces to this point
applyForce(force) {
if (!this.pinned) {
let f = p5.Vector.div(force, this.mass);
this.acc.add(f);
}
}
// Update position based on verlet integration
update() {
if (this.pinned) return;
// If being dragged, skip physics
if (this.selected && mouseIsPressed && !isCutting) {
this.pos.x = mouseX;
this.pos.y = mouseY;
// Keep some velocity info to maintain momentum after release
this.prevPos.x = mouseX - 0.1;
this.prevPos.y = mouseY - 0.1;
return;
}
// Calculate velocity
this.vel = p5.Vector.sub(this.pos, this.prevPos);
// Apply damping (more realistic with velocity-dependent damping)
let speed = this.vel.mag();
if (speed > 0) {
let drop = Math.pow(DAMPING, 1 + speed * 0.01);
this.vel.mult(drop);
}
// Save current position
this.prevPos.set(this.pos.x, this.pos.y);
// Apply velocity and acceleration
this.pos.add(this.vel);
this.pos.add(this.acc);
// Reset acceleration
this.acc.set(0, 0);
}
// Constrain to stay within canvas
constrain() {
if (this.pos.x < 0) {
this.pos.x = 0;
this.prevPos.x = this.pos.x + this.vel.x * 0.3; // Bounce
}
if (this.pos.x > width) {
this.pos.x = width;
this.prevPos.x = this.pos.x + this.vel.x * 0.3; // Bounce
}
if (this.pos.y < 0) {
this.pos.y = 0;
this.prevPos.y = this.pos.y + this.vel.y * 0.3; // Bounce
}
if (this.pos.y > height) {
this.pos.y = height;
this.prevPos.y = this.pos.y + this.vel.y * 0.3; // Bounce
}
}
// Draw this point
draw() {
if (this.selected) {
// Highlight selected point
fill(255, 100, 100);
noStroke();
ellipse(this.pos.x, this.pos.y, 8);
} else {
// Normal point
fill(255);
noStroke();
ellipse(this.pos.x, this.pos.y, 4);
}
}
// Check if point is near coordinates
isNear(x, y, radius) {
return dist(this.pos.x, this.pos.y, x, y) < radius;
}
}
// Create Stick (constraint between points)
class Stick {
constructor(p1, p2) {
this.p1 = p1;
this.p2 = p2;
this.length = p5.Vector.dist(p1.pos, p2.pos);
this.broken = false;
this.stress = 0; // Track stress on this connection
}
// Resolve constraint
resolve() {
if (this.broken) return;
// Vector between points
let dx = this.p2.pos.x - this.p1.pos.x;
let dy = this.p2.pos.y - this.p1.pos.y;
// Current distance
let currentDist = sqrt(dx * dx + dy * dy);
// Prevent division by zero
if (currentDist < 0.0001) return;
// Calculate stress (how stretched it is)
this.stress = abs(currentDist - this.length) / this.length;
// Check for automatic tearing
if (currentDist > this.length + MAX_TENSION) {
this.broken = true;
return;
}
// Difference from rest length
let diff = (this.length - currentDist) / currentDist;
// Calculate adjustment
let offsetX = dx * diff * STIFFNESS;
let offsetY = dy * diff * STIFFNESS;
// Apply adjustment if not pinned or selected
// Use mass to determine distribution of force
let totalMass = this.p1.mass + this.p2.mass;
let p1Ratio = this.p2.mass / totalMass;
let p2Ratio = this.p1.mass / totalMass;
if (!this.p1.pinned && !this.p1.selected) {
this.p1.pos.x -= offsetX * p1Ratio;
this.p1.pos.y -= offsetY * p1Ratio;
}
if (!this.p2.pinned && !this.p2.selected) {
this.p2.pos.x += offsetX * p2Ratio;
this.p2.pos.y += offsetY * p2Ratio;
}
}
// Check if stick intersects with a point (for cutting)
intersectsPoint(x, y, radius) {
// Line segment to point distance calculation
let a = this.p1.pos.x;
let b = this.p1.pos.y;
let c = this.p2.pos.x;
let d = this.p2.pos.y;
// Line length squared
let line_length_sq = (c - a) * (c - a) + (d - b) * (d - b);
// If line has no length, just check distance to endpoint
if (line_length_sq < 0.0001) {
return dist(x, y, a, b) < radius;
}
// Calculate projection point
let t = ((x - a) * (c - a) + (y - b) * (d - b)) / line_length_sq;
t = constrain(t, 0, 1);
// Find closest point on line segment
let closest_x = a + t * (c - a);
let closest_y = b + t * (d - b);
// Check distance to closest point
return dist(x, y, closest_x, closest_y) < radius;
}
// Draw this stick
draw() {
if (this.broken) return;
// Color based on stress
let stressColor = lerpColor(
color(255, 255, 255, 150), // Normal: white
color(255, 0, 0, 200), // Stressed: red
min(1, this.stress * 5)
);
stroke(stressColor);
strokeWeight(1 + this.stress * 2); // Thicker when stressed
line(this.p1.pos.x, this.p1.pos.y, this.p2.pos.x, this.p2.pos.y);
strokeWeight(1);
}
}
function setup() {
createCanvas(800, 600);
// Create cloth grid of points
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
// Position points evenly, starting from top-center
let posX =
width / 2 - (GRID_SIZE / 2) * REST_LENGTH + x * REST_LENGTH;
let posY = 50 + y * REST_LENGTH;
// Pin the top row, only at corners and middle for more realistic draping
let pinned =
y === 0 &&
(x === 0 ||
x === GRID_SIZE - 1 ||
x === Math.floor(GRID_SIZE / 2));
// Create point
let point = new Point(posX, posY, pinned);
points.push(point);
}
}
// Create sticks (connections between adjacent points)
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
let i = y * GRID_SIZE + x;
// Connect to right neighbor
if (x < GRID_SIZE - 1) {
sticks.push(new Stick(points[i], points[i + 1]));
}
// Connect to bottom neighbor
if (y < GRID_SIZE - 1) {
sticks.push(new Stick(points[i], points[i + GRID_SIZE]));
}
// Add diagonal connections only for structural stability
// (real cloth has fewer diagonals)
if (x < GRID_SIZE - 1 && y < GRID_SIZE - 1 && (x + y) % 2 === 0) {
sticks.push(new Stick(points[i], points[i + GRID_SIZE + 1]));
}
}
}
}
// Apply repulsion forces between points to prevent intersection
function applyRepulsionForces() {
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
let p1 = points[i];
let p2 = points[j];
// Skip if points are connected directly (handled by constraints)
let connected = sticks.some(
(s) =>
!s.broken &&
((s.p1 === p1 && s.p2 === p2) ||
(s.p1 === p2 && s.p2 === p1))
);
if (!connected) {
let dx = p2.pos.x - p1.pos.x;
let dy = p2.pos.y - p1.pos.y;
let distSq = dx * dx + dy * dy;
if (
distSq < REPULSION_RADIUS * REPULSION_RADIUS &&
distSq > 0.01
) {
let dist = sqrt(distSq);
let force =
(1.0 - dist / REPULSION_RADIUS) * REPULSION_STRENGTH;
let fx = (dx / dist) * force;
let fy = (dy / dist) * force;
if (!p1.pinned && !p1.selected) {
p1.pos.x -= fx;
p1.pos.y -= fy;
}
if (!p2.pinned && !p2.selected) {
p2.pos.x += fx;
p2.pos.y += fy;
}
}
}
}
}
}
// Add wind forces for more interesting movement
function applyWindForces() {
// Oscillating wind strength
let time = frameCount * 0.01;
let windStrength = noise(time, 0) * 0.1;
let windDirection = noise(time, 1000) * TWO_PI;
let windForce = p5.Vector.fromAngle(windDirection);
windForce.mult(windStrength);
// Apply wind to random points occasionally
if (random() < 0.1) {
let randomPoint = random(points);
if (!randomPoint.pinned && !randomPoint.selected) {
randomPoint.applyForce(windForce);
}
}
}
function draw() {
background(25);
// Apply gravity to all points
let gravity = createVector(0, GRAVITY);
for (let point of points) {
point.applyForce(gravity);
}
// Apply occasional subtle wind
applyWindForces();
// Check for cutting
if (mouseIsPressed && isCutting) {
for (let stick of sticks) {
if (
!stick.broken &&
stick.intersectsPoint(mouseX, mouseY, CUT_RADIUS)
) {
stick.broken = true;
}
}
}
// Physics update loop - multiple iterations for stability
const ITERATIONS = 12;
for (let i = 0; i < ITERATIONS; i++) {
// Resolve all constraints
for (let stick of sticks) {
stick.resolve();
}
// Apply repulsion to prevent intersections
applyRepulsionForces();
// Make sure points stay within bounds
for (let point of points) {
point.constrain();
}
}
// Update all points
for (let point of points) {
point.update();
}
// Draw all sticks first (behind points)
for (let stick of sticks) {
stick.draw();
}
// Draw all points
for (let point of points) {
point.draw();
}
// Draw cutting cursor
if (isCutting) {
noFill();
stroke(255, 0, 0);
ellipse(mouseX, mouseY, CUT_RADIUS * 2);
}
// Show instructions
fill(255);
noStroke();
textSize(12);
text("Click on a point to select and drag it", 20, height - 60);
text(
"Press SPACE to toggle between dragging and cutting mode",
20,
height - 40
);
text(
"Cloth will tear automatically under high tension (red lines)",
20,
height - 20
);
// Show current mode
textAlign(RIGHT);
text(
`Current mode: ${isCutting ? "CUTTING" : "DRAGGING"}`,
width - 20,
height - 20
);
textAlign(LEFT);
}
function mousePressed() {
if (isCutting) return;
// Deselect any currently selected point
if (selectedPoint) {
selectedPoint.selected = false;
selectedPoint = null;
}
// Try to select a point
for (let point of points) {
if (point.isNear(mouseX, mouseY, MOUSE_CUTOFF)) {
selectedPoint = point;
point.selected = true;
break;
}
}
}
function mouseReleased() {
// Deselect point on mouse release
if (selectedPoint) {
selectedPoint.selected = false;
selectedPoint = null;
}
}
function keyPressed() {
if (keyCode === 32) {
// SPACE key
isCutting = !isCutting;
// Deselect any point when switching modes
if (selectedPoint) {
selectedPoint.selected = false;
selectedPoint = null;
}
}
}