第8讲:制作树枝开花朵的动画

首先定义一个随机数生成器,使用生成器之前,需要安装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>

此时,就长出了茂盛的花朵。右上角的控制面板可以调整参数生成不同样式的随机树。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值