随机生成曲线/波浪路径

4
我有一幅巨大的地图图片,比视口还要大,并且居中于视口。用户可以通过拖动屏幕来浏览这张地图。为了创造出一种视差效果,我在前景中使用了一张巨大的云朵图片。当用户拖动地图进行浏览时,背景和前景都会以视差方式移动。到目前为止还不错。
然而,我真正想做的是给云朵图片一个“默认”的运动轨迹,使得每次加载页面时都随机生成云朵的移动轨迹,即使用户没有进行拖拽操作,云朵也能一直移动。我知道可以通过沿着路径对前景进行动画处理来实现这一点,但我不确定如何实现。
有谁知道有什么算法可以随机生成不规则曲线波浪形路径吗?

1
你可以使用一些随机漫步和随机漂移,例如 https://en.wikipedia.org/wiki/Ornstein%E2%80%93Uhlenbeck_process,采用粗略的大步近似,并使用三次样条插值来获得较为平滑的路径。 - Lutz Lehmann
@LutzL 很有趣!! - oldboy
4个回答

7

我也使用之前答案的一个副本来实现我在评论中暗示的简化版本。

在单位圆上进行随机游走,即在角度上进行随机游走,以确定一个慢慢但随机变化的速度向量,并使用三次贝塞尔路径向前移动。

var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 600;
var ch = c.height = 400;
var cx = cw / 4, cy = ch / 2;

var angVel = v.value;
var tension = t.value;
ctx.lineWidth = 4;

var npts = 60;
var dw = Array();
var xs = Array();
var ys = Array();
var vxs = Array();
var vys = Array();

function Randomize() {
    for (var i = 0; i < npts; i++) {
        dw[i] = (2*Math.random()-1);
    }
}

function ComputePath() {
    xs[0]=cx; ys[0]=cy; 
    var angle = 0;
    for (var i = 0; i < npts; i++) {
        vxs[i]=10*Math.cos(2*Math.PI*angle);
        vys[i]=10*Math.sin(2*Math.PI*angle);
        angle = angle + dw[i]*angVel;
    }
    for (var i = 1; i < npts; i++) {
        xs[i] = xs[i-1]+3*(vxs[i-1]+vxs[i])/2; 
        ys[i] = ys[i-1]+3*(vys[i-1]+vys[i])/2;
    }
}

function Draw() {
  ctx.clearRect(0, 0, cw, ch);
  ctx.beginPath();
  ctx.moveTo(xs[0],ys[0]); 
  for (var i = 1; i < npts; i++) {
    var cp1x = xs[i-1]+tension*vxs[i-1];
    var cp1y = ys[i-1]+tension*vys[i-1];
    var cp2x = xs[i]-tension*vxs[i];
    var cp2y = ys[i]-tension*vys[i]
    ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, xs[i], ys[i]); 
  }
  ctx.stroke();
}
Randomize();
ComputePath();
Draw();

r.addEventListener("click",()=>{
  Randomize();
  ComputePath();
  Draw();
})

v.addEventListener("input",()=>{
  angVel = v.value;
  vlabel.innerHTML = ""+angVel;
  ComputePath();
  Draw();
})

t.addEventListener("input",()=>{
  tension = t.value;
  tlabel.innerHTML = ""+tension;
  Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<table>
  <tr><td>angular velocity:</td><td> <input type="range" id="v" min ="0" max = "0.5" step = "0.01" value="0.2" /></td><td id="vlabel"></td></tr>
  <tr><td>tension</td><td> <input type="range" id="t" min ="0" max = "1" step = "0.1" value="0.8" /></td><td id="tlabel"></td></tr>
  <tr><td>remix</td><td> <button id="r"> + </button></td><td></td></tr>
</table>


这个在单位圆上的随机游走部分非常好。 - Yola
有趣的方法!! - oldboy
你介意看一下这个问题吗?[https://dev59.com/a7Hma4cB1Zd3GeqPFxX3] - oldboy

4
我对能够在SO答案中绘制画布的功能印象深刻,所以我“偷了”enxaneta的代码片段并进行了一些操作(希望这是可以的)。
这个想法是生成几个随机点(xs,ys),对于路径上的每个x,插值y为y = sum{ys_i*w_i}/sum{w_i},其中w_i是x的某些插值权重函数。例如,w_i(x) = (xs_i - x)^(-2)。希望这有意义 - 如果这对你有任何兴趣,我会尝试提供更多细节。

var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 600;
var ch = c.height = 150;
var cx = cw / 2,
  cy = ch / 2;

var amplitude = a.value;
var frequency = f.value;
ctx.lineWidth = 4;

var npts = 20;
var xs = Array();
var ys = Array();
for (var i = 0; i < npts; i++) {
   xs[i] = (cw/npts)*i; 
   ys[i] = 2.0*(Math.random()-0.5)*amplitude;
}

function Draw() {
  ctx.clearRect(0, 0, cw, ch);
  ctx.beginPath();
 
  for (var x = 0; x < cw; x++) {
    y = 0.0;
    wsum = 0.0;
    for (var i = -5; i <= 5; i++) {
       xx = x;
       ii = Math.round(x/xs[1]) + i;
       if (ii < 0) { xx += cw; ii += npts; }
       if (ii >= npts) { xx -= cw; ii -= npts; }
       w = Math.abs(xs[ii] - xx);
       w = Math.pow(w, frequency); 
       y += w*ys[ii];
       wsum += w;
    }
    y /= wsum;
    //y = Math.sin(x * frequency) * amplitude;
    ctx.lineTo(x, y+cy); 
  }

  ctx.stroke();

}
Draw();

a.addEventListener("input",()=>{
  amplitude = a.value;
  for (var i = 0; i < npts; i++) {
    xs[i] = (cw/npts)*i; 
    ys[i] = 2.0*(Math.random()-0.5)*amplitude;
  }
  Draw();
})

f.addEventListener("input",()=>{
  frequency = f.value;
  Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<p>amplitude: <input type="range" id="a" min ="1" max = "100"  value="50" /></p>
<p>frequency: <input type="range" id="f" min ="-10" max = "1" step = "0.1" value="-2" hidden/></p>


这正是我所需要的。如果您有将路径的端点连接起来形成一个封闭回路的选项,那就更好了。 - oldboy
@Anthony,我已经编辑了代码以实现这种效果。思路是使随机点的缓冲区成为循环的,并仅插值附近的点,考虑到xs数组的循环性。现在路径的两端具有相同的Y坐标,并且这些边界点处的路径是平滑的。 - Georgi Gerganov
酷,我明天要尝试实现这个~ 非常感谢Georgi!! - oldboy
你介意看一下这个问题吗?(https://dev59.com/a7Hma4cB1Zd3GeqPFxX3) - oldboy

3
如果您的问题是:如何随机生成曲线或波状路径?这是我的做法:我使用输入范围类型来改变振幅和频率的值,但您也可以在加载时随机设置这些值。 希望这有所帮助。

var c = document.getElementById("c");
var ctx = c.getContext("2d");
var cw = c.width = 800;
var ch = c.height = 150;
var cx = cw / 2,
  cy = ch / 2;

var amplitude = a.value;
var frequency = f.value;
ctx.lineWidth = 4;

function Draw() {
  ctx.clearRect(0, 0, cw, ch);
  ctx.beginPath();
 
  for (var x = 0; x < cw; x++) {
    y = Math.sin(x * frequency) * amplitude;
    ctx.lineTo(x, y+cy); 
  }

  ctx.stroke();

}
Draw();

a.addEventListener("input",()=>{
  amplitude = a.value;
  Draw();
})

f.addEventListener("input",()=>{
  frequency = f.value;
  Draw();
})
canvas{border:1px solid}
<canvas id = 'c'></canvas>
<p>frequency: <input type="range" id="f" min ="0.01" max = "0.1" step = "0.001" value=".05" /></p>
<p>amplitude: <input type="range" id="a" min ="1" max = "100"  value="50" /></p>


1
谢谢,但我希望是不规则的曲线/波浪。抱歉,我应该明确说明。 - oldboy

3

确定性随机路径

不需要存储随机移动的路径。随机就像具有非常复杂的行为,对于人类来说,使某些东西看起来随机并不需要太多的复杂性。

因此,通过加入一点点的随机性,再加上复杂性,您可以轻松地给出无限不重复序列的外观,可以倒带、停止、减速、加速等。它是完全确定性的,并且只需要存储一个单一的值(如果忽略生成代码,则是终极压缩)。

复杂的循环

要将圆心周围的点移动到圆上,可以使用正弦和余弦。

例如,一个点 x,y,您想要在球体周围以距离dist和每秒钟一次的速度移动。请参见代码片段中的示例。

var px = 100; // point of rotation.
var py = 100;
const RPS = 1; // Rotations Per Second
const dist = 50; // distance from point
const radius = 25; // circle radius

function moveObj(time) { // Find rotated point and draw    
    time = (time / 1000) * PI2 *  RPS;  // convert the time to rotations per secon    
    const xx = Math.cos(time) * dist;
    const yy = Math.sin(time) * dist;       
    drawCircle(xx, yy) 
}






// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
    ctx.setTransform(1,0,0,1,px,py);
    ctx.fillStyle = "#fff";
    ctx.beginPath();
    ctx.arc(x,y,r,0,PI2);
    ctx.fill();
}
function mainLoop(time) {
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);    
    moveObj(time);         
    requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
   background : #8AF;
   border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>

接下来,让我们使用上述方法移动旋转的中心点。然后,对于球体,我们可以改变x轴旋转的相位,使其与y轴旋转不同步。这意味着球体围绕现在旋转的点旋转,而球体的旋转轴与此不同步。

结果是更复杂的运动。

var px = 100; // point of rotation.
var py = 100;
const RPS_P = 0.1; // point Rotations Per Second 0.1 every 10 seconds
const RPS_X = 1; // Rotations Per Second in x axis of circle
const RPS_Y = 0.8; // Rotations Per Second in y axis of circle
const dist_P = 30; // distance from center point is
const dist = 50; // distance from point
const radius = 25; // circle radius

function moveObj(time) { // Find rotated point and draw    
    var phaseX = (time / 1000) * PI2 * RPS_X;
    var phaseY = (time / 1000) * PI2 * RPS_Y;
    const xx = Math.cos(phaseX) * dist;
    const yy = Math.sin(phaseY) * dist;       
    drawCircle(xx, yy) 
}

function movePoint(time) { // move point around center
    time = (time / 1000) * PI2 *  RPS_P; 
    px = 100 + Math.cos(time) * dist_P;
    py = 100 + Math.sin(time) * dist_P;       
 
}


// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
    ctx.setTransform(1,0,0,1,px,py);
    ctx.fillStyle = "#fff";
    ctx.beginPath();
    ctx.arc(x,y,r,0,PI2);
    ctx.fill();
}
function mainLoop(time) {
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);    
    movePoint(time);
    moveObj(time);         
    requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
   background : #8AF;
   border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>

我们可以继续添加反相旋转。在下一个例子中,我们现在将旋转点围绕中心进行旋转,将反相旋转添加到该点,最后以其反相旋转绘制球。

var px = 100; // point of rotation.
var py = 100;
const RPS_C_X = 0.43; // Rotation speed X of rotating rotation  point
const RPS_C_Y = 0.47; // Rotation speed Y of rotating rotation  point
const RPS_P_X = 0.093; // point Rotations speed X
const RPS_P_Y = 0.097; // point Rotations speed Y
const RPS_X = 1; // Rotations Per Second in x axis of circle
const RPS_Y = 0.8; // Rotations Per Second in y axis of circle
const dist_C = 20; // distance from center point is
const dist_P = 30; // distance from center point is
const dist = 30; // distance from point
const radius = 25; // circle radius

function moveObj(time) { // Find rotated point and draw    
    var phaseX = (time / 1000) * PI2 * RPS_X;
    var phaseY = (time / 1000) * PI2 * RPS_Y;
    const xx = Math.cos(phaseX) * dist;
    const yy = Math.sin(phaseY) * dist;       
    drawCircle(xx, yy) 
}

function movePoints(time) { // Move the rotating pointe and rotate the rotation point 
                            // around that point

    var phaseX = (time / 1000) * PI2 * RPS_C_X;
    var phaseY = (time / 1000) * PI2 * RPS_C_Y;
    px = 100 + Math.cos(phaseX) * dist_C;
    py = 100 + Math.sin(phaseY) * dist_C;  

    phaseX = (time / 1000) * PI2 * RPS_P_X;
    phaseY = (time / 1000) * PI2 * RPS_P_Y;
    px = px + Math.cos(phaseX) * dist_P;
    py = py + Math.sin(phaseY) * dist_P;       
 
}


// Helpers
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function drawCircle(x,y,r = radius) {
    ctx.setTransform(1,0,0,1,px,py);
    ctx.fillStyle = "#fff";
    ctx.beginPath();
    ctx.arc(x,y,r,0,PI2);
    ctx.fill();
}
function mainLoop(time) {
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);    
    movePoints(time);
    moveObj(time);         
    requestAnimationFrame(mainLoop);
}
const PI = Math.PI;
const PI2 = PI * 2;
canvas {
   background : #8AF;
   border : 1px solid black;
}
<canvas id="canvas" width="200" height="200"></canvas>

现在我们有一个非常复杂的旋转。位置是时间的函数,您可以通过将时间设置回起点来重复移动。您不需要存储长时间复杂的路径。

添加一点随机性

您可能会看到一些重复的运动,但如果您使用质数来设置每个旋转速率,则重复模式将作为质数的乘积循环(如果您使用大甚至小的质数,大多数人将无法告诉何时重复)。

如果您想要许多具有不同运动的对象,您可以随机化旋转速率和任何其他属性。

种子随机

Javascript没有种子随机生成器。但是,您可以创建一个。

下一个代码片段显示了一个基本的随机生成器。

const seededRandom = (() => {
  var seed = 1;
  return { 
    max : 2576436549074795, 
    reseed (s) { 
      seed = s; 
    }, 
    random ()  { 
      return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max; 
    }
  };
})();

seededRandom.random() 返回一个从 0 到 seededRandom.max - 1 的随机整数。

seededRandom.random() / seededRandom.max 返回一个从 0 到 1(不包括 1)的随机双精度浮点数。

每个种子都创建了一个可重复的唯一随机值序列。

它并不完美(不要用它来加密敏感数据、模拟真实世界的随机性、制作老虎机),但对于视觉/行为随机性来说已经足够了。

使用种子随机生成器,您可以使用种子生成随机对象。

再次使用相同的种子,您将获得相同的对象。在下面的示例中,我使用从 0 到 10000000 的种子创建云。这意味着有 10000000 个独特的可重复云。

确定性随机云的示例

使用上述所有方法和相同的 seededRandom 对象,以下是可重复的种子序列的示例。

重新启动它,它将完全重复。要将其更改为非确定性随机,请添加 randSeed(Math.random() * 100000 | 0)

const seededRandom = (() => {
    var seed = 1;
    return { max : 2576436549074795, reseed (s) { seed = s }, random ()  { return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max }}
})();
const randSeed = (seed) => seededRandom.reseed(seed|0);
const randSI = (min = 2, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS  = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const randSPow  = (min, max = min + (min = 0), p = 2) => (max + min) / 2 + (Math.pow(seededRandom.random() / seededRandom.max, p) * (max - min) * 0.5) * (randSI(2) < 1 ? 1 : -1);

const ctx = canvas.getContext("2d");
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const DIAG = (W * W + H * H) ** 0.5;
const colors = {
    dark : {
        minRGB : [100 * 0.6,200 * 0.6,240 * 0.6],
        maxRGB : [255 * 0.6,255 * 0.6,255 * 0.6],
    },
    light : {
        minRGB : [100,200,240],
        maxRGB : [255,255,255],
    },
}
const getCol = (pos, range) => "rgba(" +
    ((range.maxRGB[0] - range.minRGB[0]) * pos + range.minRGB[0] | 0) + "," + 
    ((range.maxRGB[1] - range.minRGB[1]) * pos + range.minRGB[1] | 0) + "," + 
    ((range.maxRGB[2] - range.minRGB[2]) * pos + range.minRGB[2] | 0) + "," +(pos * 0.2 + 0.8) + ")";


const Cloud = {
    x : 0,
    y : 0,
    dir : 0, // in radians
    wobble : 0,
    wobble1 : 0,
    wSpeed : 0,
    wSpeed1 : 0,
    mx : 0,  // Move offsets
    my : 0,
    seed : 0,
    size : 2, 
    detail : null,
    reset : true, // when true could resets
    init() {
        this.seed = randSI(10000000);
        this.reset = false;
        var x,y,r,dir,dist,f;
        if (this.detail === null) { this.detail = [] }
        else { this.detail.length = 0 }
        randSeed(this.seed);
        this.size = randSPow(2, 8);  // The pow add bias to smaller values
        var col = (this.size -2) / 6;
        this.col1 = getCol(col,colors.dark)
        this.col2 = getCol(col,colors.light)
        var flufCount = randSI(5,15);
        while (flufCount--) {
            x = randSI(-this.size * 8, this.size * 8);
            r = randS(this.size * 2, this.size * 8);
            dir = randS(Math.PI * 2);
            dist = randSPow(1) * r ;
            this.detail.push(f = {x,r,y : 0,mx:0,my:0, move : randS(0.001,0.01), phase : randS(Math.PI * 2)});
            f.x+= Math.cos(dir) * dist;
            f.y+= Math.sin(dir) * dist;

        }
        this.xMax = this.size * 12 + this.size * 10 + this.size * 4;
        this.yMax = this.size * 10 + this.size * 4;
        this.wobble = randS(Math.PI * 2);
        this.wSpeed = randS(0.01,0.02);
        this.wSpeed1 = randS(0.01,0.02);
        const aOff = randS(1) * Math.PI * 0.5 - Math.PI *0.25;
        this.x = W / 2 - Math.cos(this.dir+aOff) * DIAG * 0.7;
        this.y = H / 2 - Math.sin(this.dir+aOff) * DIAG * 0.7;
        clouds.sortMe = true; // flag that coulds need resort
    },
    move() {
        var dx,dy;
        this.dir = gTime / 10000;
        if(this.reset) { this.init() }
        this.wobble += this.wSpeed;
        this.wobble1 += this.wSpeed1;
        this.mx = Math.cos(this.wobble) * this.size * 4;
        this.my = Math.sin(this.wobble1) * this.size * 4;
        this.x += dx = Math.cos(this.dir) * this.size / 5;
        this.y += dy = Math.sin(this.dir) * this.size / 5;
        if (dx > 0 && this.x > W + this.xMax ) { this.reset = true }
        else if (dx < 0 && this.x < - this.xMax ) { this.reset = true }
        if (dy > 0 && this.y > H + this.yMax) { this.reset = true }
        else if (dy < 0 && this.y < - this.yMax) { this.reset = true }

        
    },
    draw(){
        const s = this.size;
        const s8 = this.size * 8;
        ctx.fillStyle = this.col1;
        ctx.setTransform(1,0,0,1,this.x+ this.mx,this.y +this.my);
        ctx.beginPath();
        for (const fluf of this.detail) {
            fluf.phase += fluf.move + Math.sin(this.wobble * this.wSpeed1) * 0.02 *  Math.cos(fluf.phase);
            fluf.mx = Math.cos(fluf.phase) * fluf.r / 2;
            fluf.my = Math.sin(fluf.phase) * fluf.r / 2;
            const x = fluf.x + fluf.mx;
            const y = fluf.y + fluf.my;
            ctx.moveTo(x + fluf.r + s, y);
            ctx.arc(x,y,fluf.r+ s,0,Math.PI * 2);
        }
        ctx.fill();
        ctx.fillStyle = this.col2;
        ctx.globalAlpha = 0.5;
        ctx.beginPath();
        for (const fluf of this.detail) {
            const x = fluf.x + fluf.mx - s;
            const y = fluf.y + fluf.my - s;
            ctx.moveTo(x + fluf.r, y);
            ctx.arc(x,y,fluf.r,0,Math.PI * 2);
        }
        ctx.fill();
        ctx.globalAlpha = 0.6;
        ctx.beginPath();
        for (const fluf of this.detail) {
            const x = fluf.x + fluf.mx - s * 1.4;
            const y = fluf.y + fluf.my - s * 1.4;
            ctx.moveTo(x + fluf.r * 0.8, y);
            ctx.arc(x,y,fluf.r* 0.8,0,Math.PI * 2);
        }
        ctx.fill();        
        ctx.globalAlpha = 1;
    }
}

function createCloud(size){ return {...Cloud} }

const clouds = Object.assign([],{
    move() { for(const cloud of this){ cloud.move() } },
    draw() { for(const cloud of this){ cloud.draw() } },
    sortMe : true, // if true then needs to resort
    resort() { 
        this.sortMe = false;
        this.sort((a,b)=>a.size - b.size);
    }
});
for(let i = 0; i < 15; i ++) { clouds.push(createCloud(40)) }
requestAnimationFrame(mainLoop)
var gTime = 0;
function mainLoop() {
    gTime += 16;
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
    if(clouds.sortMe) { clouds.resort() }
    clouds.move();
    clouds.draw();

    requestAnimationFrame(mainLoop);

}
body { padding : 0px; margin : 0px;}
canvas {
   background : rgb(60,120,148);
   border : 1px solid black;
}
<canvas id="canvas" width="600" height="200"></canvas>


1
这真是巧妙的设计。多么独特而文雅的方法啊。 - oldboy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接