Javascript Canvas 内的分形树动画
Animating a fractal tree inside a Javascript Canvas
在 youtube 上看到 Coding Train 中关于分形树的视频后,我尝试自己构建一个。效果很好,我使用了一些变量来获得不同的结果。
我很想看到树像被风吹过一样移动。我尝试了不同的方法,比如稍微旋转树枝或一些小的物理实现,但都失败了。
所以我的问题是:渲染分形树并赋予它某种 "life" 效果的最佳方法是什么,就像风中的轻微摇晃一样。
有什么好的参考吗?
我需要物理吗? -> 如果是这样,我必须去哪里看?
如果不是 -> 我怎么能伪造这样的效果?
我很高兴能得到每一个帮助。
风中的树。
以下是在风中重新弯曲树枝的一些短点。由于整个解决方案很复杂,您将不得不从代码中获取您能获取的信息。
该代码包含种子随机数函数。随机递归树渲染器,质量差的随机风力发电机,全部使用动画循环在 canvas 上绘制。
风
要应用风,您需要向每个树枝添加一个弯曲力,该弯曲力与树枝与风的角度成正比。
因此,如果您在 dir
方向有一个分支并且在 wDir
方向有风,则弯曲力需要的缩放量是
var x = Math.cos(dir); // get normalize vector for the branch
var y = Math.sin(dir);
var wx = Math.cos(wDir); // get normalize vector for the wind
var wy = Math.sin(wDir);
var forceScale = x * wy - y * wx;
树枝的长度也会影响力的大小,您将树枝的向量加长成与其长度成正比
var x = Math.cos(dir) * length; // get normalize vector for the branch
var y = Math.sin(dir) * length;
var wx = Math.cos(wDir); // get normalize vector for the wind
var wy = Math.sin(wDir);
var forceScale = x * wy - y * wx;
使用此方法可确保树枝不会被风弯曲。
还有树枝的粗细,这是一个与横截面积有关的多项式关系。这是未知的,因此被缩放到树的最大厚度(假设树基不能弯曲的近似值,但末端树枝可以弯曲很多。)
然后弯曲树枝的弹力将有一个力将树枝移回其正常位置。这就像 spring 一样,与风力非常相似。由于计算和内存负载将开始压倒 CPU,我们可以作弊并利用风来稍微 springiness 后坐力。
还有那棵树。
树需要是随机的,但由于是分形的,您不想存储每个分支。所以你还需要一个种子随机生成器,它可以在每个渲染过程开始时重置。每次迭代都会随机渲染树,但是因为每次获得相同的树时随机数都从相同的种子开始。
例子
随机绘制树和阵风。风是随机的,所以树可能不会马上移动。
单击树图像以重新播种树的随机种子值。
我没有看视频,但这些东西很标准,所以递归函数应该与你可能拥有的相去不远。我确实看到了 youTube 的封面图片,看起来这棵树没有随机性。要消除随机性,请将 leng
、ang
、width
最小值和最大值设置为相同。例如 angMin = angMax = 0.4;
将删除随机分支角度。
风力将达到旋风强度(美国为飓风)以查看最大效果。
有无数的幻数,最重要的是带有注释的常量。
const ctx = canvas.getContext("2d");
// click function to reseed random tree
canvas.addEventListener("click",()=> {
treeSeed = Math.random() * 10000 | 0;
treeGrow = 0.1; // regrow tree
});
/* Seeded random functions
randSeed(int) int is a seed value
randSI() random integer 0 or 1
randSI(max) random integer from 0 <= random < max
randSI(min, max) random integer from min <= random < max
randS() like Math.random
randS(max) random float 0 <= random < max
randS(min, max) random float min <= random < max
*/
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;
/* TREE CONSTANTS all angles in radians and lengths/widths are in pixels */
const angMin = 0.01; // branching angle min and max
const angMax= 0.6;
const lengMin = 0.8; // length reduction per branch min and max
const lengMax = 0.9;
const widthMin = 0.6; // width reduction per branch min max
const widthMax = 0.8;
const trunkMin = 6; // trunk base width ,min and max
const trunkMax = 10;
const maxBranches = 200; // max number of branches
const windX = -1; // wind direction vector
const windY = 0;
const bendability = 8; // greater than 1. The bigger this number the more the thin branches will bend first
// the canvas height you are scaling up or down to a different sized canvas
const windStrength = 0.01 * bendability * ((200 ** 2) / (canvas.height ** 2)); // wind strength
// The wind is used to simulate branch spring back the following
// two number control that. Note that the sum on the two following should
// be below 1 or the function will oscillate out of control
const windBendRectSpeed = 0.01; // how fast the tree reacts to the wing
const windBranchSpring = 0.98; // the amount and speed of the branch spring back
const gustProbability = 1/100; // how often there is a gust of wind
// Values trying to have a gusty wind effect
var windCycle = 0;
var windCycleGust = 0;
var windCycleGustTime = 0;
var currentWind = 0;
var windFollow = 0;
var windActual = 0;
// The seed value for the tree
var treeSeed = Math.random() * 10000 | 0;
// Vars to build tree with
var branchCount = 0;
var maxTrunk = 0;
var treeGrow = 0.01; // this value should not be zero
// Starts a new tree
function drawTree(seed) {
branchCount = 0;
treeGrow += 0.02;
randSeed(seed);
maxTrunk = randSI(trunkMin, trunkMax);
drawBranch(canvas.width / 2, canvas.height, -Math.PI / 2, canvas.height / 5, maxTrunk);
}
// Recusive tree
function drawBranch(x, y, dir, leng, width) {
branchCount ++;
const treeGrowVal = (treeGrow > 1 ? 1 : treeGrow < 0.1 ? 0.1 : treeGrow) ** 2 ;
// get wind bending force and turn branch direction
const xx = Math.cos(dir) * leng * treeGrowVal;
const yy = Math.sin(dir) * leng * treeGrowVal;
const windSideWayForce = windX * yy - windY * xx;
// change direction by addition based on the wind and scale to
// (windStrength * windActual) the wind force
// ((1 - width / maxTrunk) ** bendability) the amount of bending due to branch thickness
// windSideWayForce the force depending on the branch angle to the wind
dir += (windStrength * windActual) * ((1 - width / maxTrunk) ** bendability) * windSideWayForce;
// draw the branch
ctx.lineWidth = width;
ctx.beginPath();
ctx.lineTo(x, y);
x += Math.cos(dir) * leng * treeGrowVal;
y += Math.sin(dir) * leng * treeGrowVal;
ctx.lineTo(x, y);
ctx.stroke();
// if not to thing, not to short and not to many
if (branchCount < maxBranches && leng > 5 && width > 1) {
// to stop recusive bias (due to branch count limit)
// random select direction of first recusive bend
const rDir = randSI() ? -1 : 1;
treeGrow -= 0.2;
drawBranch(
x,y,
dir + randS(angMin, angMax) * rDir,
leng * randS(lengMin, lengMax),
width * randS(widthMin, widthMax)
);
// bend next branch the other way
drawBranch(
x,y,
dir + randS(angMin, angMax) * -rDir,
leng * randS(lengMin, lengMax),
width * randS(widthMin, widthMax)
);
treeGrow += 0.2;
}
}
// Dont ask this is a quick try at wind gusts
// Wind needs a spacial component this sim does not include that.
function updateWind() {
if (Math.random() < gustProbability) {
windCycleGustTime = (Math.random() * 10 + 1) | 0;
}
if (windCycleGustTime > 0) {
windCycleGustTime --;
windCycleGust += windCycleGustTime/20
} else {
windCycleGust *= 0.99;
}
windCycle += windCycleGust;
currentWind = (Math.sin(windCycle/40) * 0.6 + 0.4) ** 2;
currentWind = currentWind < 0 ? 0 : currentWind;
windFollow += (currentWind - windActual) * windBendRectSpeed;
windFollow *= windBranchSpring ;
windActual += windFollow;
}
requestAnimationFrame(update);
function update() {
ctx.clearRect(0,0,canvas.width,canvas.height);
updateWind();
drawTree(treeSeed);
requestAnimationFrame(update);
}
body {
font-family : arial;
}
<canvas id="canvas" width="250" heigth="200"></canvas>
Click tree to reseed.
更新
我刚刚注意到风和树枝的长度是绝对的,因此在更大的树上画树 canvas 会产生太大的弯曲力,树枝会弯曲超过风矢量。
要扩大 sim 的规模,要么通过全局尺度变换进行,要么将 windStrength
常数减小到某个较小的值。您将不得不使用该值作为其二阶多项式关系。我的猜测是将它乘以 (200 ** 2) / (canvas.height ** 2)
,其中 200 是示例 canvas 的大小,canvas.height
是新的 canvas 大小。
我已将计算添加到示例中,但它并不完美,因此当您缩放时,如果弯曲太远或不弯曲,则必须向下或向上更改值 windStrength
(第一个数字)够了。
在 youtube 上看到 Coding Train 中关于分形树的视频后,我尝试自己构建一个。效果很好,我使用了一些变量来获得不同的结果。
我很想看到树像被风吹过一样移动。我尝试了不同的方法,比如稍微旋转树枝或一些小的物理实现,但都失败了。
所以我的问题是:渲染分形树并赋予它某种 "life" 效果的最佳方法是什么,就像风中的轻微摇晃一样。 有什么好的参考吗? 我需要物理吗? -> 如果是这样,我必须去哪里看? 如果不是 -> 我怎么能伪造这样的效果?
我很高兴能得到每一个帮助。
风中的树。
以下是在风中重新弯曲树枝的一些短点。由于整个解决方案很复杂,您将不得不从代码中获取您能获取的信息。
该代码包含种子随机数函数。随机递归树渲染器,质量差的随机风力发电机,全部使用动画循环在 canvas 上绘制。
风
要应用风,您需要向每个树枝添加一个弯曲力,该弯曲力与树枝与风的角度成正比。
因此,如果您在 dir
方向有一个分支并且在 wDir
方向有风,则弯曲力需要的缩放量是
var x = Math.cos(dir); // get normalize vector for the branch
var y = Math.sin(dir);
var wx = Math.cos(wDir); // get normalize vector for the wind
var wy = Math.sin(wDir);
var forceScale = x * wy - y * wx;
树枝的长度也会影响力的大小,您将树枝的向量加长成与其长度成正比
var x = Math.cos(dir) * length; // get normalize vector for the branch
var y = Math.sin(dir) * length;
var wx = Math.cos(wDir); // get normalize vector for the wind
var wy = Math.sin(wDir);
var forceScale = x * wy - y * wx;
使用此方法可确保树枝不会被风弯曲。
还有树枝的粗细,这是一个与横截面积有关的多项式关系。这是未知的,因此被缩放到树的最大厚度(假设树基不能弯曲的近似值,但末端树枝可以弯曲很多。)
然后弯曲树枝的弹力将有一个力将树枝移回其正常位置。这就像 spring 一样,与风力非常相似。由于计算和内存负载将开始压倒 CPU,我们可以作弊并利用风来稍微 springiness 后坐力。
还有那棵树。
树需要是随机的,但由于是分形的,您不想存储每个分支。所以你还需要一个种子随机生成器,它可以在每个渲染过程开始时重置。每次迭代都会随机渲染树,但是因为每次获得相同的树时随机数都从相同的种子开始。
例子
随机绘制树和阵风。风是随机的,所以树可能不会马上移动。
单击树图像以重新播种树的随机种子值。
我没有看视频,但这些东西很标准,所以递归函数应该与你可能拥有的相去不远。我确实看到了 youTube 的封面图片,看起来这棵树没有随机性。要消除随机性,请将 leng
、ang
、width
最小值和最大值设置为相同。例如 angMin = angMax = 0.4;
将删除随机分支角度。
风力将达到旋风强度(美国为飓风)以查看最大效果。
有无数的幻数,最重要的是带有注释的常量。
const ctx = canvas.getContext("2d");
// click function to reseed random tree
canvas.addEventListener("click",()=> {
treeSeed = Math.random() * 10000 | 0;
treeGrow = 0.1; // regrow tree
});
/* Seeded random functions
randSeed(int) int is a seed value
randSI() random integer 0 or 1
randSI(max) random integer from 0 <= random < max
randSI(min, max) random integer from min <= random < max
randS() like Math.random
randS(max) random float 0 <= random < max
randS(min, max) random float min <= random < max
*/
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;
/* TREE CONSTANTS all angles in radians and lengths/widths are in pixels */
const angMin = 0.01; // branching angle min and max
const angMax= 0.6;
const lengMin = 0.8; // length reduction per branch min and max
const lengMax = 0.9;
const widthMin = 0.6; // width reduction per branch min max
const widthMax = 0.8;
const trunkMin = 6; // trunk base width ,min and max
const trunkMax = 10;
const maxBranches = 200; // max number of branches
const windX = -1; // wind direction vector
const windY = 0;
const bendability = 8; // greater than 1. The bigger this number the more the thin branches will bend first
// the canvas height you are scaling up or down to a different sized canvas
const windStrength = 0.01 * bendability * ((200 ** 2) / (canvas.height ** 2)); // wind strength
// The wind is used to simulate branch spring back the following
// two number control that. Note that the sum on the two following should
// be below 1 or the function will oscillate out of control
const windBendRectSpeed = 0.01; // how fast the tree reacts to the wing
const windBranchSpring = 0.98; // the amount and speed of the branch spring back
const gustProbability = 1/100; // how often there is a gust of wind
// Values trying to have a gusty wind effect
var windCycle = 0;
var windCycleGust = 0;
var windCycleGustTime = 0;
var currentWind = 0;
var windFollow = 0;
var windActual = 0;
// The seed value for the tree
var treeSeed = Math.random() * 10000 | 0;
// Vars to build tree with
var branchCount = 0;
var maxTrunk = 0;
var treeGrow = 0.01; // this value should not be zero
// Starts a new tree
function drawTree(seed) {
branchCount = 0;
treeGrow += 0.02;
randSeed(seed);
maxTrunk = randSI(trunkMin, trunkMax);
drawBranch(canvas.width / 2, canvas.height, -Math.PI / 2, canvas.height / 5, maxTrunk);
}
// Recusive tree
function drawBranch(x, y, dir, leng, width) {
branchCount ++;
const treeGrowVal = (treeGrow > 1 ? 1 : treeGrow < 0.1 ? 0.1 : treeGrow) ** 2 ;
// get wind bending force and turn branch direction
const xx = Math.cos(dir) * leng * treeGrowVal;
const yy = Math.sin(dir) * leng * treeGrowVal;
const windSideWayForce = windX * yy - windY * xx;
// change direction by addition based on the wind and scale to
// (windStrength * windActual) the wind force
// ((1 - width / maxTrunk) ** bendability) the amount of bending due to branch thickness
// windSideWayForce the force depending on the branch angle to the wind
dir += (windStrength * windActual) * ((1 - width / maxTrunk) ** bendability) * windSideWayForce;
// draw the branch
ctx.lineWidth = width;
ctx.beginPath();
ctx.lineTo(x, y);
x += Math.cos(dir) * leng * treeGrowVal;
y += Math.sin(dir) * leng * treeGrowVal;
ctx.lineTo(x, y);
ctx.stroke();
// if not to thing, not to short and not to many
if (branchCount < maxBranches && leng > 5 && width > 1) {
// to stop recusive bias (due to branch count limit)
// random select direction of first recusive bend
const rDir = randSI() ? -1 : 1;
treeGrow -= 0.2;
drawBranch(
x,y,
dir + randS(angMin, angMax) * rDir,
leng * randS(lengMin, lengMax),
width * randS(widthMin, widthMax)
);
// bend next branch the other way
drawBranch(
x,y,
dir + randS(angMin, angMax) * -rDir,
leng * randS(lengMin, lengMax),
width * randS(widthMin, widthMax)
);
treeGrow += 0.2;
}
}
// Dont ask this is a quick try at wind gusts
// Wind needs a spacial component this sim does not include that.
function updateWind() {
if (Math.random() < gustProbability) {
windCycleGustTime = (Math.random() * 10 + 1) | 0;
}
if (windCycleGustTime > 0) {
windCycleGustTime --;
windCycleGust += windCycleGustTime/20
} else {
windCycleGust *= 0.99;
}
windCycle += windCycleGust;
currentWind = (Math.sin(windCycle/40) * 0.6 + 0.4) ** 2;
currentWind = currentWind < 0 ? 0 : currentWind;
windFollow += (currentWind - windActual) * windBendRectSpeed;
windFollow *= windBranchSpring ;
windActual += windFollow;
}
requestAnimationFrame(update);
function update() {
ctx.clearRect(0,0,canvas.width,canvas.height);
updateWind();
drawTree(treeSeed);
requestAnimationFrame(update);
}
body {
font-family : arial;
}
<canvas id="canvas" width="250" heigth="200"></canvas>
Click tree to reseed.
更新
我刚刚注意到风和树枝的长度是绝对的,因此在更大的树上画树 canvas 会产生太大的弯曲力,树枝会弯曲超过风矢量。
要扩大 sim 的规模,要么通过全局尺度变换进行,要么将 windStrength
常数减小到某个较小的值。您将不得不使用该值作为其二阶多项式关系。我的猜测是将它乘以 (200 ** 2) / (canvas.height ** 2)
,其中 200 是示例 canvas 的大小,canvas.height
是新的 canvas 大小。
我已将计算添加到示例中,但它并不完美,因此当您缩放时,如果弯曲太远或不弯曲,则必须向下或向上更改值 windStrength
(第一个数字)够了。