import Model from "./Model"; import CollisionGrid from "./CollisionGrid"; const { ccclass, property } = cc._decorator; type BezierObject = { pos1: cc.Vec2, pos2: cc.Vec2, pos3: cc.Vec2, pos4: cc.Vec2, con1: cc.Vec2, con2: cc.Vec2, con3: cc.Vec2, con4: cc.Vec2, } @ccclass export default class MetaballMgr extends cc.Component { @property(cc.Node) targerParent: cc.Node = null; _grid: CollisionGrid; private _graphics: cc.Graphics; private _waters: Array; onLoad() { var size = cc.view.getVisibleSize(); this._grid = new CollisionGrid(size.width, size.height, 30); } start() { this._graphics = this.getComponent(cc.Graphics); this.draw(); } set waters(value) { this._waters = value; } get waters() { return this._waters; } draw() { if (Model.setting.showWaterEff == false) return this.checkCollision2() } checkCollision() { this._grid.assign(this._waters) var numChecks: number = this._grid.checks.length; this._graphics.clear(); this._graphics.fillColor = cc.color(14, 137, 240); for (var i = 0; i < numChecks; i += 2) { var ball = this._grid.checks[i] let bassPos1 = ball.position; this._graphics.circle(bassPos1.x, bassPos1.y, 15); this._graphics.fill(); var anotherBall = this._grid.checks[i + 1] let bassPos2 = anotherBall.position; let bezierObj: BezierObject = null; if (bassPos1.y < bassPos2.y) { bezierObj = this.metaball(15, 15, bassPos1, bassPos2); } else { bezierObj = this.metaball(15, 15, bassPos2, bassPos1); } if (bezierObj) { this._graphics.moveTo(bezierObj.pos1.x, bezierObj.pos1.y); this._graphics.bezierCurveTo(bezierObj.con1.x, bezierObj.con1.y, bezierObj.con3.x, bezierObj.con3.y, bezierObj.pos3.x, bezierObj.pos3.y); this._graphics.lineTo(bezierObj.pos4.x, bezierObj.pos4.y); this._graphics.bezierCurveTo(bezierObj.con4.x, bezierObj.con4.y, bezierObj.con2.x, bezierObj.con2.y, bezierObj.pos2.x, bezierObj.pos2.y); this._graphics.lineTo(bezierObj.pos1.x, bezierObj.pos1.y); this._graphics.fill(); } } } checkCollision2() { this._graphics.clear(); this._graphics.fillColor = cc.color(14, 137, 240); for (let i = 0; i < this._waters.length; i++) { let ball = this._waters[i]; let bassPos1 = ball.position; let radius1 = 15; this._graphics.circle(bassPos1.x, bassPos1.y, radius1); this._graphics.fill(); for (let j = i; j < this._waters.length; j++) { if (i === j) { continue; } let anotherBall = this._waters[j]; let bassPos2 = anotherBall.position; let radius2 = 15; let bezierObj: BezierObject = null; if (bassPos1.y < bassPos2.y) { bezierObj = this.metaball(radius1, radius2, bassPos1, bassPos2); } else { bezierObj = this.metaball(radius2, radius1, bassPos2, bassPos1); } if (bezierObj) { this._graphics.moveTo(bezierObj.pos1.x, bezierObj.pos1.y); this._graphics.bezierCurveTo(bezierObj.con1.x, bezierObj.con1.y, bezierObj.con3.x, bezierObj.con3.y, bezierObj.pos3.x, bezierObj.pos3.y); this._graphics.lineTo(bezierObj.pos4.x, bezierObj.pos4.y); this._graphics.bezierCurveTo(bezierObj.con4.x, bezierObj.con4.y, bezierObj.con2.x, bezierObj.con2.y, bezierObj.pos2.x, bezierObj.pos2.y); this._graphics.lineTo(bezierObj.pos1.x, bezierObj.pos1.y); this._graphics.fill(); } } } } metaball(radius1, radius2, center1, center2, handleSize = 2.4): BezierObject { const HALF_PI = Math.PI / 2; const d = center1.sub(center2).mag() const maxDist = radius1 + radius2 * 1.9; const v = (maxDist - d) / maxDist * 2.2 + 0.4 let u1, u2; // No blob if a radius is 0 // or if distance between the circles is larger than max-dist // or if circle2 is completely inside circle1 if (radius1 === 0 || radius2 === 0 || d > maxDist || d <= Math.abs(radius1 - radius2)) { return null; } // Calculate u1 and u2 if the circles are overlapping if (d < radius1 + radius2) { u1 = Math.acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d)); u2 = Math.acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d)); } else { // Else set u1 and u2 to zero u1 = 0; u2 = 0; } // Calculate the max spread let angleBetweenCenters = center1.sub(center2).angle(cc.v2(-1, 0)); if (center1.y > center2.y) { angleBetweenCenters = -angleBetweenCenters; } const maxSpread = Math.acos((radius1 - radius2) / d); // Angles for the points const angle1 = angleBetweenCenters + u1 + (maxSpread - u1) * v; const angle2 = angleBetweenCenters - u1 - (maxSpread - u1) * v; const angle3 = angleBetweenCenters + Math.PI - u2 - (Math.PI - u2 - maxSpread) * v; const angle4 = angleBetweenCenters - Math.PI + u2 + (Math.PI - u2 - maxSpread) * v; // Point locations const p1 = this.getVector(center1, angle1, radius1); const p2 = this.getVector(center1, angle2, radius1); const p3 = this.getVector(center2, angle3, radius2); const p4 = this.getVector(center2, angle4, radius2); // Define handle length by the distance between both ends of the curve const totalRadius = radius1 + radius2; const d2Base = Math.min(v * handleSize, p1.sub(p3).mag() / totalRadius); // Take into account when circles are overlapping const d2 = d2Base * Math.min(1, d * 2 / (radius1 + radius2)); // Length of the handles const r1 = radius1 * d2; const r2 = radius2 * d2; // Handle locations const h1 = this.getVector(p1, angle1 - HALF_PI, r1); const h2 = this.getVector(p2, angle2 + HALF_PI, r1); const h3 = this.getVector(p3, angle3 + HALF_PI, r2); const h4 = this.getVector(p4, angle4 - HALF_PI, r2); // Generate the connector path return { pos1: p1, pos2: p2, pos3: p3, pos4: p4, con1: h1, con2: h2, con3: h3, con4: h4, }; } metaballToPath(p1, p2, p3, p4, h1, h2, h3, h4, escaped, r) { //C: 曲线 A:弧线 C 曲线 // return ['M', p1, 'C', h1, h3, p3, 'A', r, r, 0, escaped ? 1 : 0, 0, p4, 'C', h4, h3, p4].join(' '); return [p1, p2, p3, p4, h1, h2, h3, h4, escaped ? 1 : 0, r]; } dist(p1, p2) { return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5; } getVector(vec: cc.Vec2, angle: number, radius: number) { let offX = radius * Math.cos(angle); let offY = radius * Math.sin(angle); return cc.v2(vec.x + offX, vec.y + offY); } }