首先定义一个随机数生成器,使用生成器之前,需要安装seedrandom包
npm install seedrandom
在package.json种,这是我安装的所有依赖:
{
"name": "gamelets",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"@types/dat.gui": "^0.7.13",
"@types/seedrandom": "^3.0.8",
"eslint": "^9.5.0",
"typescript": "^5.4.5",
"vite": "^5.3.1"
},
"dependencies": {
"dat.gui": "^0.7.9",
"pixi.js": "^7.0.4",
"seedrandom": "^3.0.5"
}
}
在src/lib目录下,定义RandomGenerator.ts
import seedrandom from 'seedrandom';
export class RandomGenerator{
public _prng: () => number;
constructor(seed: number){
this._prng = seedrandom(seed.toString());
}
public next(): number {
return this._prng();
}
public nextBetween(start: number, end: number): number{
return Math.random() * (end - start) + start;
}
public nextInt(_range: number){
return Math.floor(Math.random() * _range);
}
}
在src目录新建一个tree-generator目录,定义一个Branch.ts
import {Graphics, Point} from "pixi.js";
import {RandomGenerator} from "../lib/RandomGenerator";
import { TreeGenerator } from "./TreeGenrator";
import '../lib/PointUtils'
export class Branch{
rng: RandomGenerator;
children: Branch[] = [];
graphics = new Graphics();
drawnPercent = 0;
constructor(public tree: TreeGenerator, public options:{
position: Point,
angle: number,
size: number,
length: number,
seed: number,
color: number,
}){
this.rng = new RandomGenerator(options.seed);
tree.app.stage.addChild(this.graphics);
}
destroy(): void{
this.graphics.destroy();
this.children.forEach(child => child.destroy());
}
getEndPosition(): Point{
const options = this.options;
// 转换生长角度为弧度
const radians = options.angle / 180 * Math.PI;
// 计算树枝头尾的向量
let vector = new Point(options.length).rotate(radians);
// 尾端 = 起点 + 生长向量
return options.position.add(vector);
}
createOneBranch(): Branch[]{
const options = this.options;
const treeOps = this.tree.options;
const rng = this.rng;
//计算新树枝的生长方向。
let angle = options.angle + rng.nextBetween(-20, 20);
// 新枝要变得更细一些
let size = options.size - 1;
// 长度用size去计算(size越小,长度越短)
let length = (size + 10) / (treeOps.trunkSize + 3) * 80;
// 再把长度加一点点盐
length *= rng.nextBetween(0.5, 1);
// 创建新枝
let branch = new Branch(this.tree, {
position: this.getEndPosition(),
angle: angle,
size: size,
length: length,
seed: rng.nextInt(999999),
color: options.color
});
// 以为阵列的方式返回一根新枝条
return [branch];
}
createTwoBranches(): Branch[]{
const options = this.options;
const treeOps = this.tree.options;
const rng = this.rng;
let branches: Branch[] = [];
// 计算新枝的生长方向
let angleAvg = options.angle + rng.nextBetween(-20, 20);
// 两根树枝的夹角在30到90度之间
let angleInBetween = rng.nextBetween(30, 90);
// 计算两根树枝的生长方向
let angles = [
angleAvg - angleInBetween / 2,
angleAvg + angleInBetween / 2
];
// 新枝要细一些
let size = options.size - 1;
// 长度是用size去计算的(size越小,长度越短)
let length = (size + 10) / (treeOps.trunkSize + 3) * 80;
for(let angle of angles){
let branch = new Branch(
this.tree,
{
position: this.getEndPosition(),
angle: angle,
size: size,
length: length * rng.nextBetween(0.5, 1),
seed: rng.nextInt(999999),
color: options.color
}
);
branches.push(branch);
}
return branches;
}
createPetals(): Branch[]{
const options = this.options;
const treeOps = this.tree.options;
const rng = this.rng;
let petals: Branch[] = [];
// 花瓣构成的圆的总角度
let anglesTotal = 240;
//花瓣数量
let count = 8;
// 花瓣之间的夹角
let angleInterval = anglesTotal / (count - 1);
// 第一片花瓣的角度
let startAngle = options.angle - anglesTotal / 2;
// 循环count次,生长出所有花瓣
for(let i = 0; i < count; i++){
let petal = new Branch(this.tree, {
position: this.getEndPosition(),
angle: startAngle + i * angleInterval,
size: 3, // 花瓣粗细
length: 15, // 花瓣长度
seed:rng.nextInt(999999),
color: treeOps.flowerColor
});
petals.push(petal);
}
return petals;
}
createLeaves(): Branch[]{
const options = this.options;
const treeOps = this.tree.options;
const rng = this.rng;
let leaves: Branch[] = [];
// 沿着树枝,每6个单位长度一片叶子
let interval = 6;
// 转换树枝方向的单位为弧度,等一下计算向量时需要用到
const radians = options.angle / 180 * Math.PI;
// 叶子与树枝之间的夹角
let angleToLeaf = 60;
//从离起点0距离开始,每次循环加点距离,知道超出树枝范围时离开
for(let dist = 0; dist < options.length; dist += interval){
// 计算叶子离起点的向量
let vector = new Point(dist).rotate(radians);
// 叶子出生的位置 = 树枝起点 + 距离向量
let leafPos = options.position.add(vector);
// 随机选择叶子在树枝的左边还是右边(50%左边,50%右边)
let rightSide = rng.next() > 0.5;
// 计算叶子的生长角度
let leafAngle = options.angle +
(rightSide ? angleToLeaf : -angleToLeaf);
// 造一片叶子
let leaf = new Branch(this.tree, {
position: leafPos,
angle: leafAngle,
size: 3,
length: 5 + options.size,
seed: rng.nextInt(999999),
color: treeOps.leafColor,
});
leaves.push(leaf);
}
return leaves;
}
createChildren(): void{
const options = this.options;
const treeOps = this.tree.options;
const rng = this.rng;
//粗细大于1才会长出子枝
if(options.size > 1){
// 乱数决定下一段是单枝还是要分两枝
if(rng.next() < treeOps.branchRate){
// 要分两枝
this.children = this.createTwoBranches();
}else{
// 只要单枝
this.children = this.createOneBranch();
}
// 让子枝继续长出子枝
this.children.forEach(child => child.createChildren());
}else{
// 最细的树枝要长花
let petals = this.createPetals();
// petal也是一段Branch只是参数不同
this.children = this.children.concat(petals);
}
// 如果这个树枝足够细,就长叶子
if(options.size <= treeOps.leafBranchSize){
let leaves = this.createLeaves();
// leaf也是一段Branch,只是参数不同
this.children = this.children.concat(leaves);
}
}
draw(percent: number): void{
if(this.drawnPercent == percent){
// 画完了
return;
}
const options = this.options;
const start = options.position;
// 树枝生长方向的向量 = 生长终点 - 起点
let vector = this.getEndPosition().sub(start);
// 在生长到percent时的终点
let end = new Point(start.x + vector.x * percent, start.y + vector.y * percent);
// 准备画线,线清除之前画的东西
this.graphics.clear();
// 设定画线笔刷
this.graphics.lineStyle({
width: options.size,
color: options.color
});
// 移动笔刷到起点(这行不会画出线条)
this.graphics.moveTo(start.x, start.y);
// 画线到终点
this.graphics.lineTo(end.x, end.y);
// 记录现在画到哪里了
this.drawnPercent = percent;
}
drawDeeply(timepassed: number): void{
const options = this.options;
const treeOps = this.tree.options;
// 画完本枝需要的时间 = 本枝长度 / 画图速度
let timeToComplete = options.length / treeOps.drawSpeed;
// 需要画出来的进度, 限制进度最大到1
let percent = Math.min(1, timepassed / timeToComplete);
// 画出本枝
this.draw(percent);
// 把经过时间减掉画完本枝需要的时间
timepassed -= timeToComplete;
if(timepassed > 0){
this.children.forEach(child => child.drawDeeply(timepassed));
}
}
}
依然是tree-generator目录,定义TreeGenerator.ts
import {Application, Point} from "pixi.js";
import { Branch } from "./Branch";
import { OptionsEditor } from "./OptionsEditor";
function getStageSize(): any{
let stageObj = {width: window.innerWidth, height: window.innerHeight};
return stageObj;
}
export class TreeGenerator{
// 植树参数
options = {
seed: 5, // 孵化种子
trunkSize: 12, //主干粗细
trunkLength: 400, //主干长度
branchRate: 0.72, //分支概率
drawSpeed: 5, //生长速度
leafBranchSize: 7, //这个粗细以下的树枝会长叶子
branchColor: 0xFFFFFF, //树枝颜色
leafColor: 0x00AA00, // 叶子颜色
flowerColor: 0xFF6666, //花的颜色
};
drawingData?: {
mainTrunk: Branch, // 树的主干
timepassed: number // 画图的经过时间
};
newTree(): void{
if(this.drawingData){
// 如果之前有旧的树,把旧的树砍了
this.drawingData.mainTrunk.destroy();
}
const treeOps = this.options;
const stageSize = getStageSize();
// 计算舞台左右置中的地步位置,作为主板的出生位置
let treePos = new Point(stageSize.width / 2, stageSize.height);
// 种一颗新树
let mainTrunk = new Branch(
this, {
position: treePos,
angle: -90,
size: treeOps.trunkSize,
length: treeOps.trunkLength,
seed: treeOps.seed,
color: treeOps.branchColor
}
);
// 让主干去开枝散叶
mainTrunk.createChildren();
// 初始化绘图重画需要的资料
this.drawingData = {
mainTrunk: mainTrunk,
timepassed: 0
};
}
drawUpdate(deltaTime: number): void{
const data = this.drawingData;
if (data){
data.timepassed += deltaTime;
data.mainTrunk.drawDeeply(data.timepassed);
if(data.timepassed > 1000){
this.app.ticker.stop();
}
}
}
constructor(public app: Application){
// 建立一颗新树
this.newTree();
app.ticker.add(this.drawUpdate, this);
// 建立参数面板
new OptionsEditor(this);
}
}
在src根路径,定义一个TreeGenMain.ts,这是入口。
import { RandomGenerator } from "./lib/RandomGenerator";
import { Branch } from "./tree-generator/Branch";
import { TreeGenerator } from "./tree-generator/TreeGenrator";
import { Application, Graphics } from "pixi.js";
import './style.css'
// app.view就是画布,因为已经指定了泛型:HTMLCanvasElement
let app = new Application<HTMLCanvasElement>();
document.body.appendChild(app.view);
let stageFrame = new Graphics();
app.stage.addChild(stageFrame);
app.renderer.resize(window.innerWidth, window.innerHeight);
let treeGen = new TreeGenerator(app);
在使用参数控制面板之前,需要安装2个包
npm install dat.gui
npm install -D @types/dat.gui
在src/tree-generator目录下,定义OjptionsEditor.ts
import {GUI} from "dat.gui"
import { TreeGenerator } from "./TreeGenrator"
export class OptionsEditor{
gui = new GUI();
constructor(public generator: TreeGenerator){
let options = generator.options;
this.gui.add(options, 'seed', 1, 99999, 1);
this.gui.add(this, 'onButtonGrow').name('ReGenerate');
this.gui.add(this, 'onButtonNext').name('Generate Next Tree')
this.gui.add(options, 'trunkSize', 1, 16, 1);
this.gui.add(options, 'trunkLength', 1, 400, 1);
this.gui.add(options, 'branchRate', 0,1,0.01);
this.gui.add(options, 'drawSpeed', 1, 20, 1);
this.gui.add(options, 'leafBranchSize', 1, 10, 1);
this.gui.addColor(options, 'branchColor');
this.gui.addColor(options, 'leafColor');
this.gui.addColor(options, 'flowerColor');
}
onButtonGrow(){
// 要检查是否正在渲染
if(!this.generator.app.ticker.started){
this.generator.app.ticker.start();
}
this.generator.newTree();
}
onButtonNext(){
// 要检查是否正在渲染
if(!this.generator.app.ticker.started){
this.generator.app.ticker.start();
}
this.generator.options.seed ++;
this.generator.newTree();
// 根据改变的seed变量,重新获取并显示
this.gui.updateDisplay();
}
}
当然,必须要修改index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/TreeGenMain.ts"></script>
</body>
</html>
此时,就长出了茂盛的花朵。右上角的控制面板可以调整参数生成不同样式的随机树。