├── style.scss ├── index.html └── Cloth.js /style.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background: #f2f2f2; 8 | } 9 | 10 | #canvas { 11 | display: block; 12 | } 13 | 14 | span, div { 15 | position: absolute; 16 | color: #aaa; 17 | bottom: 100px; 18 | left: 0; 19 | right: 0; 20 | width: 100%; 21 | margin: auto; 22 | font-family: Helvetica; 23 | text-align: center; 24 | } 25 | 26 | div { 27 | bottom: 60px; 28 | a { 29 | text-decoration: none; 30 | color: #2266bb; 31 | &:first-child { 32 | margin-right: 20px; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | Drag with your mouse, right-click to slice. 11 |
15 | -------------------------------------------------------------------------------- /Cloth.js: -------------------------------------------------------------------------------- 1 | window.requestAnimFrame = 2 | window.requestAnimationFrame || 3 | window.webkitRequestAnimationFrame || 4 | window.mozRequestAnimationFrame || 5 | window.oRequestAnimationFrame || 6 | window.msRequestAnimationFrame || 7 | function (callback) { 8 | window.setTimeout(callback, 1e3 / 60) 9 | } 10 | 11 | let accuracy = 5 12 | let gravity = 400 13 | let clothY = 28 14 | let clothX = 54 15 | let spacing = 8 16 | let tearDist = 60 17 | let friction = 0.99 18 | let bounce = 0.5 19 | 20 | let canvas = document.getElementById('canvas') 21 | let ctx = canvas.getContext('2d') 22 | 23 | canvas.width = window.innerWidth 24 | canvas.height = window.innerHeight 25 | 26 | ctx.strokeStyle = '#555' 27 | 28 | let mouse = { 29 | cut: 8, 30 | influence: 26, 31 | down: false, 32 | button: 1, 33 | x: 0, 34 | y: 0, 35 | px: 0, 36 | py: 0 37 | } 38 | 39 | class Point { 40 | constructor (x, y) { 41 | this.x = x 42 | this.y = y 43 | this.px = x 44 | this.py = y 45 | this.vx = 0 46 | this.vy = 0 47 | this.pinX = null 48 | this.pinY = null 49 | 50 | this.constraints = [] 51 | } 52 | 53 | update (delta) { 54 | if (this.pinX && this.pinY) return this 55 | 56 | if (mouse.down) { 57 | let dx = this.x - mouse.x 58 | let dy = this.y - mouse.y 59 | let dist = Math.sqrt(dx * dx + dy * dy) 60 | 61 | if (mouse.button === 1 && dist < mouse.influence) { 62 | this.px = this.x - (mouse.x - mouse.px) 63 | this.py = this.y - (mouse.y - mouse.py) 64 | } else if (dist < mouse.cut) { 65 | this.constraints = [] 66 | } 67 | } 68 | 69 | this.addForce(0, gravity) 70 | 71 | let nx = this.x + (this.x - this.px) * friction + this.vx * delta 72 | let ny = this.y + (this.y - this.py) * friction + this.vy * delta 73 | 74 | this.px = this.x 75 | this.py = this.y 76 | 77 | this.x = nx 78 | this.y = ny 79 | 80 | this.vy = this.vx = 0 81 | 82 | if (this.x >= canvas.width) { 83 | this.px = canvas.width + (canvas.width - this.px) * bounce 84 | this.x = canvas.width 85 | } else if (this.x <= 0) { 86 | this.px *= -1 * bounce 87 | this.x = 0 88 | } 89 | 90 | if (this.y >= canvas.height) { 91 | this.py = canvas.height + (canvas.height - this.py) * bounce 92 | this.y = canvas.height 93 | } else if (this.y <= 0) { 94 | this.py *= -1 * bounce 95 | this.y = 0 96 | } 97 | 98 | return this 99 | } 100 | 101 | draw () { 102 | let i = this.constraints.length 103 | while (i--) this.constraints[i].draw() 104 | } 105 | 106 | resolve () { 107 | if (this.pinX && this.pinY) { 108 | this.x = this.pinX 109 | this.y = this.pinY 110 | return 111 | } 112 | 113 | this.constraints.forEach((constraint) => constraint.resolve()) 114 | } 115 | 116 | attach (point) { 117 | this.constraints.push(new Constraint(this, point)) 118 | } 119 | 120 | free (constraint) { 121 | this.constraints.splice(this.constraints.indexOf(constraint), 1) 122 | } 123 | 124 | addForce (x, y) { 125 | this.vx += x 126 | this.vy += y 127 | } 128 | 129 | pin (pinx, piny) { 130 | this.pinX = pinx 131 | this.pinY = piny 132 | } 133 | } 134 | 135 | class Constraint { 136 | constructor (p1, p2) { 137 | this.p1 = p1 138 | this.p2 = p2 139 | this.length = spacing 140 | } 141 | 142 | resolve () { 143 | let dx = this.p1.x - this.p2.x 144 | let dy = this.p1.y - this.p2.y 145 | let dist = Math.sqrt(dx * dx + dy * dy) 146 | 147 | if (dist < this.length) return 148 | 149 | let diff = (this.length - dist) / dist 150 | 151 | if (dist > tearDist) this.p1.free(this) 152 | 153 | let mul = diff * 0.5 * (1 - this.length / dist) 154 | 155 | let px = dx * mul 156 | let py = dy * mul 157 | 158 | !this.p1.pinX && (this.p1.x += px) 159 | !this.p1.pinY && (this.p1.y += py) 160 | !this.p2.pinX && (this.p2.x -= px) 161 | !this.p2.pinY && (this.p2.y -= py) 162 | 163 | return this 164 | } 165 | 166 | draw () { 167 | ctx.moveTo(this.p1.x, this.p1.y) 168 | ctx.lineTo(this.p2.x, this.p2.y) 169 | } 170 | } 171 | 172 | class Cloth { 173 | constructor () { 174 | this.points = [] 175 | 176 | let startX = canvas.width / 2 - clothX * spacing / 2 177 | 178 | for (let y = 0; y <= clothY; y++) { 179 | for (let x = 0; x <= clothX; x++) { 180 | let point = new Point(startX + x * spacing, 20 + y * spacing) 181 | y === 0 && point.pin(point.x, point.y) 182 | x !== 0 && point.attach(this.points[this.points.length - 1]) 183 | y !== 0 && point.attach(this.points[x + (y - 1) * (clothX + 1)]) 184 | 185 | this.points.push(point) 186 | } 187 | } 188 | } 189 | 190 | update (delta) { 191 | let i = accuracy 192 | 193 | while (i--) { 194 | this.points.forEach((point) => { 195 | point.resolve() 196 | }) 197 | } 198 | 199 | ctx.beginPath() 200 | this.points.forEach((point) => { 201 | point.update(delta * delta).draw() 202 | }) 203 | ctx.stroke() 204 | } 205 | } 206 | 207 | function setMouse (e) { 208 | let rect = canvas.getBoundingClientRect() 209 | mouse.px = mouse.x 210 | mouse.py = mouse.y 211 | mouse.x = e.clientX - rect.left 212 | mouse.y = e.clientY - rect.top 213 | } 214 | 215 | canvas.onmousedown = (e) => { 216 | mouse.button = e.which 217 | mouse.down = true 218 | setMouse(e) 219 | } 220 | 221 | canvas.onmousemove = setMouse 222 | 223 | canvas.onmouseup = () => (mouse.down = false) 224 | 225 | canvas.oncontextmenu = (e) => e.preventDefault() 226 | 227 | let cloth = new Cloth() 228 | 229 | ;(function update (time) { 230 | ctx.clearRect(0, 0, canvas.width, canvas.height) 231 | 232 | cloth.update(0.016) 233 | 234 | window.requestAnimFrame(update) 235 | })(0) 236 | --------------------------------------------------------------------------------