2016-09-17 80 views
1

我試圖找出一種算法繪製這種隨機的形狀,知道可以在其中最大內接矩形的大小:算法畫圖隨機漫畫式的雲

comic-style cloud

我希望有可變的文本適合雲內,其中一些可能很短,而另一些可能長一些句子;這就是爲什麼我不能去預定義的大小。

我目前的做法是:

  1. 創建可以在矩形使用其最短邊的圓的直徑刻,最大的圓。
  2. 創建另一個隨機大小的圓,其中心位於第一個圓的邊緣某處。
  3. 找到兩個圓之間的交點並選擇一個隨機的圓。
  4. 放置另一個以選定點爲中心的隨機大小的圓。找到與之前的圓的交點(圓圈所覆蓋的除外)並重復,直到所有剩餘的交點出現在矩形外。

我的希望是,通過在其他圓圈之間的交點處放置圓圈,我將以比通過將它們放置在矩形內的隨機位置時更少的步驟覆蓋所需的所有區域,放置在已被前面的圈子完全覆蓋的區域中的圈子。

一審已經取得了這個醜陋的東西:

ugliest cloud

但我發現在這兩個問題:

  1. 即使所有剩餘的交叉路口發生的矩形之外,並不是所有的它被覆蓋,因爲它的左下角在白色圓圈下方以藍色可見。
  2. 我不知道如何繪製帶有小缺口的輪廓。

有沒有人知道隨機繪製這些形狀的機制?謝謝。

編輯:

繼et_l的很好的建議,我已經把這個不理想的實現,給還是不很理想的結果,但多了,比我自己的初步試驗好得多:

function vector(a, b) 
 
{ 
 
    if (!(this instanceof vector)) return new vector(a, b); 
 
    if (a instanceof vector && b instanceof vector) { 
 
     this.x = b.x - a.x; 
 
     this.y = b.y - a.y; 
 
    } 
 
    else { 
 
     this.x = a; 
 
     this.y = b; 
 
    } 
 
} 
 

 
vector.prototype = 
 
{ 
 
    get lengthSq() 
 
    { 
 
     return this.x * this.x + this.y * this.y; 
 
    }, 
 
    get length() 
 
    { 
 
     return Math.sqrt(this.lengthSq); 
 
    }, 
 
    get angle() 
 
    { 
 
     return (Math.PI/2) + Math.atan2(this.x, this.y); 
 
    }, 
 
    distanceTo:function(x, y) 
 
    { 
 
     if (arguments.length == 1) return new vector(this, arguments[0]).length; 
 
     return new vector(this, new vector(x, y)).length; 
 
    }, 
 
    clone: function() 
 
    { 
 
     return new vector(this.x, this.y); 
 
    }, 
 
    normalize: function() 
 
    { 
 
     var l = this.length; 
 
     this.x /= l; 
 
     this.y /= l; 
 
     return this; 
 
    }, 
 
    get normalized() 
 
    { 
 
     return this.clone().normalize(); 
 
    }, 
 
    add: function(x, y) 
 
    { 
 
     if (x instanceof vector) return this.add(x.x, x.y); 
 
     this.x += x; 
 
     this.y += y; 
 
     return this; 
 
    }, 
 
    scale: function(x, y) 
 
    { 
 
     if (y === undefined) y = x; 
 
     this.x *= x; 
 
     this.y *= y; 
 
     return this; 
 
    }, 
 
    scaled: function(x, y) 
 
    { 
 
     return this.clone().scale(x, y); 
 
    } 
 
}; 
 

 
vector.add = function(v1, v2) 
 
{ 
 
    return v1.clone().add(v2); 
 
}; 
 

 
vector.dot = function(v1, v2) 
 
{ 
 
    return v1.x * v2.x + v1.y * v2.y; 
 
}; 
 

 
vector.cross = function(v1, v2) 
 
{ 
 
    return v1.x * v2.y - v1.y * v2.x; 
 
} 
 

 
function line(p1, p2) 
 
{ 
 
    if (!(this instanceof line)) { 
 
     if (arguments.length == 0) return new line(); 
 
     if (arguments.length == 2) return new line(arguments[0], arguments[1]); 
 
     if (arguments.length == 4) return new line(arguments[0], arguments[1], arguments[2], arguments[3]) 
 
    } 
 
    if (arguments.length == 0) { 
 
     this.p1 = new vector(0, 0); 
 
     this.p2 = new vector(0, 0);  
 
    } 
 
    else if (arguments.length == 2) { 
 
     this.p1 = p1; 
 
     this.p2 = p2;  
 
    } 
 
    else if (arguments.length == 4) { 
 
     this.p1 = new vector(arguments[0], arguments[1]); 
 
     this.p2 = new vector(arguments[2], arguments[3]); 
 
    } 
 
} 
 

 
line.prototype = 
 
{ 
 
    get angle() 
 
    { 
 
     return new vector(this.p1, this.p2).angle; 
 
    }, 
 
    get lengthSq() 
 
    { 
 
     return new vector(this.p1, this.p2).lengthSq; 
 
    }, 
 
    get length() 
 
    { 
 
     return new vector(this.p1, this.p2).length; 
 
    }, 
 
    distanceTo: function(p, extend) 
 
    { 
 
     var pp; 
 
     var v1 = new vector(this.p1, p); 
 
     var v2 = new vector(this.p1, this.p2); 
 
     var v2len2 = v2.lengthSq; 
 
     var disc = v2len2 == 0 ? -1 : vector.dot(v1, v2)/v2len2; 
 
     if (!extend && disc < 0) pp = this.p1; 
 
     else if (!extend && disc > 1) pp = this.p2; 
 
     else pp = vector.add(this.p1, v2.scaled(disc)); 
 
     return new vector(p, pp).length; 
 
    }, 
 
    intersect:function(other) 
 
    { 
 
     var otx, oty, tdx, tdy, odx, ody, cross1, cross2, cross3, t; 
 
     tdx = this.p2.x - this.p1.x; 
 
     tdy = this.p2.y - this.p1.y; 
 
     odx = other.p2.x - other.p1.x; 
 
     ody = other.p2.y - other.p1.y; 
 
     cross1 = tdx * ody - odx * tdy; 
 
     if (cross1 == 0) return null; 
 
     var overZero = cross1 > 0; 
 
     otx = this.p1.x - other.p1.x; 
 
     oty = this.p1.y - other.p1.y; 
 
     cross2 = tdx * oty - tdy * otx; 
 
     if (cross2 < 0 == overZero) return null; 
 
     cross3 = odx * oty - ody * otx; 
 
     if ((cross3 < 0) == overZero) return null; 
 
     if ((cross2 > cross1 == overZero) || (cross3 > cross1 == overZero)) return null; 
 
     t = cross3/cross1; 
 
     var r = { x:undefined, y:undefined }; 
 
     r.x = this.p1.x + (t * tdx); 
 
     r.y = this.p1.y + (t * tdy); 
 
     return r; 
 
    } 
 
}; 
 

 
function ellipse(x, y, width, height, angle, stroke, fill, precision) 
 
{ 
 
    this.x = x; 
 
    this.y = y; 
 
    this.width = width; 
 
    this.height = height; 
 
    this.angle = angle; 
 
    if (stroke) { 
 
     var els = stroke.split(' '); 
 
     this.stroke = els[0]; 
 
     if (els.length > 1) this.lineWidth = parseFloat(els[1]); 
 
    } 
 
    this.fill = fill; 
 
    this.precision = precision || 5; 
 
} 
 

 
ellipse.prototype = 
 
{ 
 
    get center() 
 
    { 
 
     return new vector(this.x, this.y); 
 
    }, 
 
    setStroke: function(value) 
 
    { 
 
     var els = value.split(' '); 
 
     this.stroke = els[0]; 
 
     if (els.length > 1) this.lineWidth = parseFloat(els[1]); 
 
     return this; 
 
    }, 
 
    setFill: function(value) 
 
    { 
 
     this.fill = value; 
 
     return this; 
 
    }, 
 
    clone: function() 
 
    { 
 
     return new ellipse(this.x, this.y, this.width, this.height, this.angle, this.stroke + (this.lineWidth !== undefined ? ' ' + this.lineWidth : ''), this.fill, this.precision) 
 
    }, 
 
    angleAt: function(x, y) 
 
    { 
 
     if (arguments.length == 1) return new vector(x.x - this.x, x.y - this.y).angle; 
 
     return new vector(x - this.x, y - this.y).angle; 
 
    }, 
 
    pointAt: function(angle) 
 
    { 
 
     var cost = Math.cos(this.angle), sint = Math.sin(this.angle); 
 
     var cosa = Math.cos(angle), sina = Math.sin(angle); 
 
     var x = this.x + (this.width * cosa * cost - this.height * sina * sint); 
 
     var y = this.y + (this.width * cosa * sint + this.height * sina * cost); 
 
     return new vector(x, y); 
 
    }, 
 
    inflate: function(x, y) 
 
    { 
 
     if (y === undefined) y = x; 
 
     if (typeof x == 'string') { 
 
      if (x.substr(-1) == '%') this.width *= 1 + parseFloat(x)/100; 
 
      else this.width += parseFloat(x); 
 
     } 
 
     else this.width += x; 
 
     if (typeof y == 'string') { 
 
      if (y.substr(-1) == '%') this.height *= 1 + parseFloat(y)/100; 
 
      else this.height += parseFloat(y); 
 
     } 
 
     else this.height += y; 
 
     return this; 
 
    }, 
 
    randomPoint: function() 
 
    { 
 
     return this.pointAt(Math.random() * Math.PI * 2); 
 
    }, 
 
    intersect: function(other) 
 
    { 
 
     var r = []; 
 
     var lt = new line(), ot = new line(); 
 
     var tcos = Math.cos(this.angle), tsin = Math.sin(this.angle); 
 
     var ocos = Math.cos(other.angle), osin = Math.sin(other.angle); 
 
     lt.p1 = { x:this.x + (this.width * tcos), y:this.y + (this.width * tsin) }; 
 
     var o0 = { x:other.x + (other.width * ocos), y:other.y + (other.width * osin) }; 
 
     for (var ta = 1; ta < 360; ta += this.precision) { 
 
      var x, y, trads = ta * Math.PI/180; 
 
      x = this.x + (this.width * Math.cos(trads) * tcos - this.height * Math.sin(trads) * tsin); 
 
      y = this.y + (this.width * Math.cos(trads) * tsin + this.height * Math.sin(trads) * tcos); 
 
      lt.p2 = { x:x, y:y }; 
 
      ot.p1 = o0; 
 
      for (var oa = 1; oa < 360; oa += other.precision) { 
 
       var orads = oa * Math.PI/180; 
 
       x = other.x + (other.width * Math.cos(orads) * ocos - other.height * Math.sin(orads) * osin); 
 
       y = other.y + (other.width * Math.cos(orads) * osin + other.height * Math.sin(orads) * ocos); 
 
       ot.p2 = { x:x, y:y }; 
 
       var i = lt.intersect(ot); 
 
       if (i) r.push(i); 
 
       ot.p1 = ot.p2; 
 
      } 
 
      ot.p2 = { x:other.x + (other.width * ocos), y:other.y + (other.width * osin) }; 
 
      var i = lt.intersect(ot); 
 
      if (i) r.push(i); 
 
      lt.p1 = lt.p2; 
 
     } 
 
     lt.p2 = { x:this.x + (this.width * tcos), y:this.y + (this.width * tsin) }; 
 
     ot.p1 = o0; 
 
     for (var oa = 1; oa < 360; oa += other.precision) { 
 
      var orads = oa * Math.PI/180; 
 
      x = other.x + (other.width * Math.cos(orads) * ocos - other.height * Math.sin(orads) * osin); 
 
      y = other.y + (other.width * Math.cos(orads) * osin + other.height * Math.sin(orads) * ocos); 
 
      ot.p2 = { x:x, y:y }; 
 
      var i = lt.intersect(ot); 
 
      if (i) r.push(i); 
 
      ot.p1 = ot.p2; 
 
     } 
 
     ot.p2 = { x:other.x + (other.width * ocos), y:other.y + (other.width * osin) }; 
 
     var i = lt.intersect(ot); 
 
     if (i) r.push(i); 
 
     return r; 
 
    }, 
 
    draw: function(ctx) 
 
    { 
 
     var cos = Math.cos(this.angle), sin = Math.sin(this.angle); 
 
     var p0 = { x:this.x + (this.width * cos), y:this.y + (this.width * sin) }; 
 
     ctx.beginPath(); 
 
     ctx.moveTo(p0.x, p0.y); 
 
     for (var a = 1; a < 360; a += this.precision) { 
 
      var rads = a * Math.PI/180; 
 
      var x = this.x + (this.width * Math.cos(rads) * cos - this.height * Math.sin(rads) * sin); 
 
      var y = this.y + (this.width * Math.cos(rads) * sin + this.height * Math.sin(rads) * cos); 
 
      var p1 = { x:x, y:y }; 
 
      ctx.lineTo(p1.x, p1.y); 
 
     } 
 
     ctx.closePath(); 
 
     if (this.fill) { 
 
      ctx.fillStyle = this.fill; 
 
      ctx.fill(); 
 
     } 
 
     if (this.stroke) { 
 
      if (this.lineWidth) ctx.lineWidth = this.lineWidth; 
 
      ctx.strokeStyle = this.stroke; 
 
      ctx.stroke(); 
 
     } 
 
    }, 
 
    drawIntersections:function(other, ctx, color, width) 
 
    { 
 
     var intersections = this.intersect(other); 
 
     ctx.fillStyle = color || 'white'; 
 
     if (width === undefined) width = 5; 
 
     for (var i = 0; i < intersections.length; ++i) { 
 
      ctx.fillRect(intersections[i].x - width/2, intersections[i].y - width/2, width, width); 
 
     } 
 
    } 
 
}; 
 

 
function cloud(x, y, width, height) 
 
{ 
 
    this.x = x; 
 
    this.y = y; 
 
    this.width = width; 
 
    this.height = height; 
 

 
    var center = { x: x + width/2, y: y + height/2 }; 
 
    if (Math.random() >= .5) { 
 
     var diagonal = new line(x, y, x + width, y + height); 
 
     var disth = diagonal.distanceTo(new vector(this.x + width, this.y), true); 
 
    } 
 
    else { 
 
     var diagonal = new line(x + width, y, x, y + height); 
 
     var disth = diagonal.distanceTo(new vector(this.x, this.y), true); 
 
    } 
 
    var distw = diagonal.length/2; 
 
    var angle = diagonal.angle; 
 
    this.cover = new ellipse(center.x, center.y, distw, disth, angle, 'white', 'white'); 
 
    this.body = this.cover.clone().inflate(5).setStroke('black 1px'); 
 
    this.puffs = []; 
 
    var a = Math.random() * Math.PI/12; 
 
    while (a < Math.PI * 7) { 
 
     var p = this.cover.pointAt(a); 
 
     var w = (.1 + Math.random() * .2) * (distw + disth)/2; 
 
     this.puffs.push(new ellipse(p.x, p.y, w, w, 0, 'black 1px', 'white')); 
 
     a += .75; 
 
    } 
 
} 
 

 
cloud.prototype = 
 
{ 
 
    draw:function(ctx) 
 
    { 
 
     this.body.draw(ctx); 
 
     for (var i = 0; i < this.puffs.length; ++i) { 
 
      this.puffs[i].draw(ctx); 
 
     } 
 
     this.cover.draw(ctx); 
 
    } 
 
}; 
 

 
var canvas = document.querySelector('canvas'), ctx = canvas.getContext('2d'); 
 
canvas.width = canvas.offsetWidth; 
 
canvas.height = canvas.offsetHeight; 
 
var cloud = new cloud((canvas.width - 300)/2, (canvas.height - 100)/2, 300, 100); 
 
cloud.draw(ctx);
body { 
 
    margin: 0; 
 
    width: 100vw; 
 
    height: 100vh; 
 
    overflow: hidden; 
 
    background-image: linear-gradient(to bottom, #2688FF, #94C4FF 75%, #B8D8FF); 
 
} 
 

 
canvas { 
 
    width: 100vw; 
 
    height: 100vh 
 
}
<canvas></canvas>

注:

  1. 我已經去了一個傾斜的橢圓,認爲數學會很容易。男孩是我錯了!數學可能很容易,但我的數學技能是垃圾。我嘗試了幾次來推導方程以找到兩個可能旋轉的橢圓之間的交點,並且我總是以一種我不知道如何簡化的理性表達式結束:-(最後,我放棄了並決定近似同樣,一旦我找到每個添加的「撲」和較大的「身體」橢圓之間的交集,就會發現它們之間的交集,我試圖找到它們相對於橢圓中心的相應「角度」,這樣我就可以選擇一個角度更大的角度,並繼續向順時針方向添加隨機抽吸,直到我繞過整個橢圓體爲止。不知道如何計算角度,所以我以固定的時間間隔放置隨機大小的泡芙與π無關,並做了幾次革命,所以泡芙在顯然隨機的地方重疊。

回答

0

我喜歡你的方法。這裏有一個想法,以改善它,因此結果將兩者覆蓋的矩形,並具有所需的缺口:

  1. 創建具有一定傾斜,但覆蓋了整個矩形的橢圓形。如果你不想打擾傾斜,你可以使用回答here得到一個覆蓋矩形的橢圓。例如,使用the insight即基本橢圓方程實際上是單位圓的拉伸方程,您可以根據矩形使用它:(x/a)^2+(y/b)^2=1,其中a:=rectangle.width/sqrt(2)b:=rectangle.height/sqrt(2)

  2. 製作一個橢圓的副本,並在兩個軸上放大它的位置(擴展它)。你可以使用一個百分比,例如兩個軸的拉伸比例爲5%。

  3. 的大橢圓你之前做過什麼 - 開始在其圓周上任意一點,放在那裏隨機大小的圓,計算與橢圓的交點,並把新的隨機大小的圓圈中心那裏,繼續,直到你覆蓋整個周長。 這一次 - 使用對他們有黑色筆劃的圓圈。

  4. 將原始橢圓(小的)帶到整個集合形狀的前面

所以最終您保留與原來的小橢圓覆蓋矩形和阻礙圈子筆劃的其餘部分和放大的橢圓連同圓圈使與形狀內部凹口的期望輪廓。

+0

謝謝et_l。我已經做了一個你可能想調整一下的概念證明。你的建議非常有幫助。 –

+0

我只是想補充一點,最好是沿着一個橢圓繪製一堆隨機大小的橢圓,然後沿着一個沒有筆畫的較小的橢圓繪製一束隨機大小的橢圓。這樣所有的線條都不那麼平均。你甚至可以沿曲線而不是橢圓進行。而且你應該讓這些外圍的人變得更大一些,所以看起來不那麼笨拙。 – Bodhi1