[p5js创意编程] 花束

本教程主要教大家通过使用p5js等工具,来绘制出动态生成的花束,步骤如下。

  • 背景绘制
  • 花瓣绘制
  • 花朵绘制
  • 树枝绘制

下面,我将会逐一为大家进行详细地讲解,请做好笔记哦~

一、背景绘制

首先是背景绘制。

将画布看作是640x640的网格,在每个网格中绘制直径为1的圆形,而每个圆形填充颜色的透明度与该位置y坐标值成正相关,即y值越大,透明度越大。

将画面中间裁去一个圆圈,在位于圆圈里面的点的位置绘制直径为1的圆形,每个圆形填充颜色的透明度与该位置y坐标值成负相关,即y值越大,透明度越小。

同时为了让渐变看的更自然,对每个位置填充颜色的透明度加上了randomGaussian(0,10)的高斯噪声。

代码如下。

scss 复制代码
function setup() {
    createCanvas(640,640);
    background(255);
}

function draw(){
    push();
    var rc = color(random(255),random(255),random(255));
    for(var i = 0;i < width;i++){
        for(var j = 0;j < height;j++){
            // 绘制背景
            if (dist(i,j,width/2,height/2)>150) {
                var ra = constrain(map(j,0,height,-100,150),0,150)+randomGaussian(0,10);
                rc.setAlpha(ra);
                fill(rc);
                noStroke();
                ellipse(i,j,1,1);
            }else {
                // 绘制玻璃球
                var ra = constrain(map(j,0,height,100,-50),0,150)+randomGaussian(0,10);
                rc.setAlpha(ra);
                fill(rc);
                noStroke();
                ellipse(i,j,1,1);
            }
        }
    }
    pop();
    noLoop();  
}

显示效果如下。

背景绘制

然而,使用p5js原生的draw()函数绘制速度很慢,因为其是使用CPU一个像素一个像素来进行绘制的。

故我们可以使用shaders通过GPU并行计算 ,来加速绘制速度。还不会使用shaders来绘制的小伙伴,可以见我以往的翻译教程。由于这边的绘制比较简单,故我直接放上shaders代码。

ini 复制代码
<script id="vertexShader" type="x-shader/x-vertex">
    #ifdef GL_ES
    precision mediump float;
    #endif
    attribute vec3 aPosition;

    void main() {
      vec4 positionVec4 = vec4(aPosition, 1.0);
      positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
      gl_Position = positionVec4;
    }
</script>
arduino 复制代码
<script id="fragmentShader" type="x-shader/x-fragment">
    #ifdef GL_ES
    precision mediump float;
    #endif

    uniform vec2 u_resolution;
    uniform vec3 u_color;


    float random(vec2 co){
        return fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453);
    }

    float map(float x, float o_min, float o_max, float r_min, float r_max){
        return (x-o_min)/(o_max-o_min)*(r_max-r_min)+r_min;
    }

二、花瓣绘制

获取花瓣的贝塞尔曲线起始点和控制点

工具:PS

  1. 新建画布,大小最好和processing或p5js的画布大小一致
  2. 使用钢笔工具绘制出想要的花瓣形状;
  3. 将工具换成直接选择工具(快捷键 A),打开信息面板,将鼠标移至曲线控制点或起始点处,记录下信息面板中的x与y值(如下图所示)。

使用PS绘制花瓣

若出现以下情况,即一条曲线(绿色箭头所指曲线)由三个点构成,则将第一个控制点坐标为起始点,即起始点坐标、起始点坐标、第二个控制点坐标、结束点。

  1. 记录下所有点的坐标,格式为:起始点坐标、第一个控制点坐标、第二个控制点坐标、结束点(另一条曲线的起始点)、另一条曲线的第一个控制点、另一条曲线的第二个控制点、另一条曲线的结束点......如下所示。
less 复制代码
// 存放了四朵不同形状花瓣的集合
var bezierFlowerSet = [    [[261.99,323.60],[258.37,310.12],[246.21,233.20],[267.91,255.88],[288.29,278.56],
        [258.37,310.12],[261.99,323.60]],
    [[261.99,323.60],[263.31,292.04],[242.60,254.89],[266.92,254.57],[294.20,255.88],
        [263.31,292.04],[261.99,323.60]],
    [[268.06,251.14],[271.77,264.11],[242.74,296.84],[263.12,319.08],[284.94,341.93],
        [271.77,264.11],[268.06,251.14]],
    [[284.87,311.02],[317.59,302.05],[315.28,294.52],[322.81,284.67],[331.78,275.11],
        [347.13,269.61],[351.19,271.35],[354.66,273.67],[363.64,279.75],[361.90,291.91],
        [360.45,304.07],[348.87,308.71],[339.60,310.15],[332.36,311.31],[317.88,301.76],
        [284.87,311.02]],
]
  1. 通过坐标转换将花瓣的起始点从PS中的坐标位置移到我们所想要的坐标位置,即将花瓣的所有点坐标(包括起始点和控制点)全部转化为相对于第一个起始点位置的相对坐标(而不是绝对坐标) ,函数如下所示。
ini 复制代码
// i为花瓣数组的索引,即选择第i种花瓣形状
function calShape(arrSet,i) {
    var points = [];
    for(var j = 0;j < arrSet[i].length;j++){
        points.push(new Point(arrSet[i][j][0]-arrSet[i][0][0],
            arrSet[i][j][1]-arrSet[i][0][1]));
    }
    return points;
}

花瓣绘制完整代码如下所示。

scss 复制代码
// 存放了四朵不同形状花瓣的集合
var bezierFlowerSet = [    [[261.99,323.60],[258.37,310.12],[246.21,233.20],[267.91,255.88],[288.29,278.56],
        [258.37,310.12],[261.99,323.60]],
    [[261.99,323.60],[263.31,292.04],[242.60,254.89],[266.92,254.57],[294.20,255.88],
        [263.31,292.04],[261.99,323.60]],
    [[268.06,251.14],[271.77,264.11],[242.74,296.84],[263.12,319.08],[284.94,341.93],
        [271.77,264.11],[268.06,251.14]],
    [[284.87,311.02],[317.59,302.05],[315.28,294.52],[322.81,284.67],[331.78,275.11],
        [347.13,269.61],[351.19,271.35],[354.66,273.67],[363.64,279.75],[361.90,291.91],
        [360.45,304.07],[348.87,308.71],[339.60,310.15],[332.36,311.31],[317.88,301.76],
        [284.87,311.02]],
]


function setup() {
    createCanvas(640,640);
    background(255);
}

function draw(){

    for(var i = 0;i < bezierFlowerSet.length;i++){
        var shape = calShape(bezierFlowerSet,i);
        push();
        noStroke();
        fill(255,0,0,100);

        beginShape();
        var x = width/bezierFlowerSet.length*i+20;
        var y = height/2
        vertex(x,y);

        for(var k = 1;k < shape.length;k+=3){
            bezierVertex(shape[k].x + x,shape[k].y + y,
                         shape[k+1].x + x,shape[k+1].y + y,
                         shape[k+2].x + x,shape[k+2].y + y,)
        }

        endShape();
        pop();
    }


    noLoop();
}

function calShape(arrSet,i) {
    var points = [];
    for(var j = 0;j < arrSet[i].length;j++){
        points.push(new Point(arrSet[i][j][0]-arrSet[i][0][0],
            arrSet[i][j][1]-arrSet[i][0][1]));
    }
    return points;
}

class Point {
    constructor(tempX,tempY) {
        this.x = tempX;
        this.y = tempY;
    }
}

生成花瓣如下所示。

单片花瓣

三、花朵绘制

有了花瓣以后,将每一种花瓣绕其中心点旋转一周,便可得到完整的花朵,同时放大花瓣并调低其透明度,按照刚才的旋转方案进行再一次旋转,便可绘制更具艺术感的花朵。

为了添加花朵形态的多样性,我还添加了aside属性,来标明这朵花是否以侧面 来面对我们的,此属性可通过减少花瓣的数量和限制花瓣的旋转角度来实现。

具体代码如下所示。

kotlin 复制代码
class Flower {
    constructor(tempX,tempY,petalsNum,tempC,tempS) {
        var flowerNum = bezierFlowerSet.length + bigFlowerSet.length; //不同形态花瓣的数量
        this.x = tempX;
        this.y = tempY;
        this.petals = int(petalsNum);  // 在一朵花朵中花瓣的数量
        this.color = tempC;
        this.size = tempS; // 调整花朵的大小,使更加有多样性
        // 判断是否为侧向花朵
        if (random(1) < 0.7) {
            this.aside = false;
        }else {
            this.aside = true;
        }

        var shapeIndex = int(random(flowerNum)); // 选择花瓣的形态
        if (shapeIndex < bezierFlowerSet.length) {
            this.shape = calShape(bezierFlowerSet,shapeIndex);
        } else {
            this.shape = calShape(bigFlowerSet,shapeIndex-bezierFlowerSet.length);
            this.petals = int(random(8,10));
            if (!this.aside) {
                this.size /= 2;
            }
        }

        if (this.aside) {  // 设置侧向花朵的属性,即减少花瓣数量和限制旋转角度
            this.rotateA = random(PI/3,PI/2);
            this.petals = int(random(10,12));
            this.rotateAs = random(-PI/4,PI/4); // 改变花瓣的起始角度,来使得侧面花具备多样性
        }else {
            this.rotateA = TWO_PI; // 完整的花朵
        }


    }

    display(){
        // 缩放花朵大小
        translate(this.x,this.y);
        scale(this.size);
        translate(-this.x,-this.y);

        for(var j = 0;j < this.petals-1;j++){  // 绘制花朵
            push();
            translate(this.x,this.y);
            rotate(map(j,0,this.petals-1,0,this.rotateA));  // 根据不同的j来计算当前该旋转的角度
            translate(-this.x,-this.y);

            translate(this.x,this.y); // 根据花朵是否是侧向花来决定是否需要缩放
            if (this.aside) {
                scale(randomGaussian(1,0.05));
            }else {
                scale(randomGaussian(1,0.03));
            }
            translate(-this.x,-this.y);

            if (this.aside) {  // 若为侧向花,则调整初始角度
                translate(this.x,this.y);
                rotate(this.rotateAs);
                translate(-this.x,-this.y);
            }

            push();

            translate(this.x,this.y);  // 根据实际情况,再次调整花朵大小。其实感觉这边有点重复了,但懒得改了。。xD
            scale(randomGaussian(0.4,0.01));
            translate(-this.x,-this.y);

            this.color.setAlpha(200); // 设置花瓣内围透明度

            noStroke();
            fill(this.color);

            beginShape(); // 绘制内围花瓣
            vertex(this.x,this.y);

            for(var k = 1;k < this.shape.length;k+=3){
                bezierVertex(this.shape[k].x+this.x,this.shape[k].y+this.y,
                             this.shape[k+1].x+this.x,this.shape[k+1].y+this.y,
                            this.shape[k+2].x+this.x,this.shape[k+2].y+this.y,)
            }

            endShape();
            pop();

            translate(this.x,this.y);
            if (this.aside) {
                scale(randomGaussian(1,0.05));
            }else {
                scale(randomGaussian(1,0.02));
            }
            translate(-this.x,-this.y);

            beginShape(); // 绘制外围花瓣
            this.color.setAlpha(100);
            fill(this.color);
            noStroke();
            vertex(this.x,this.y);

            for(var k = 1;k < this.shape.length;k+=3){
                bezierVertex(this.shape[k].x+this.x,this.shape[k].y+this.y,
                             this.shape[k+1].x+this.x,this.shape[k+1].y+this.y,
                            this.shape[k+2].x+this.x,this.shape[k+2].y+this.y,)
            }

            endShape();
            pop();
        }
    }
}

四、树枝绘制

树枝绘制就比较简单了,就是创建一个数组,其中包含了一段树枝中的各个连接点的信息,通过将连接点以不同strokeWeight连接起来,便可实现树枝的绘制,多说无益,可以看看代码来进行理解。

kotlin 复制代码
class Branch {
    constructor(tempX,tempY,tempL) {
        this.x = tempX;  // 树枝的最底下顶点坐标
        this.y = tempY;
        this.l = tempL; // 树枝的长度

        this.points = []; // 树枝连接点数组
        this.num = 6; // 树枝连接点数量

        this.points.push(new Point(this.x,this.y)); // 将第一个点压入points数组

        for(var i = 0;i < this.num;i++){  // 越往上的树枝点就越偏离树枝中心轴的位置
            this.points.push(new Point(randomGaussian(this.x,map(i,0,this.num-1,0,5)),this.y-this.l/this.num*i+randomGaussian(0,5)));
        }
        '''
        new Point(x,y) 
        其中x为randomGaussian(this.x,map(i,0,this.num-1,0,5)) 后面这个map是用来控制正态分布的方差,简单理解就 
        是偏离树枝中心轴的位置
        y为this.y-this.l/this.num*i+randomGaussian(0,5)
        '''

        this.points = this.points.sort(function (a, b) { // 按照y值大小对连接点进行排序
        return b.y - a.y;
        })
    }

    display(){
        push();
        noFill();
        stroke(75,87,62,random(100,255));
        strokeWeight(6);
        for(var i = 1;i < this.points.length;i++){  // 使用不同的strokeWeight来绘制树枝,越往上的树枝越细
            strokeWeight(6-6/this.points.length*i);
            line(this.points[i-1].x,this.points[i-1].y,this.points[i].x,this.points[i].y);
        }
        pop();
    }
}

function draw(){
    for (var i = 0; i < branches.length; i++) {
        push();

        var roA = map(flowerNum,8,19,PI/25,PI/10); // 根据花朵的数量多少来控制树枝的旋转角度,防止太密集或者太稀疏

        // 将树枝绕其从下往上数第三个节点旋转一定的角度
        translate(branches[i].points[2].x,branches[i].points[2].y);
        var angle = map(i,0,branches.length-1,-roA,roA);
        rotate(angle);
        translate(-branches[i].points[2].x,-branches[i].points[2].y);

        branches[i].display(); // 绘制树枝
        pop();
    }

}

为了让树枝的绘制更加生动,我们通过粒子运动来实现树枝的动态绘制,效果如下所示。

树枝绘制

简单来讲,就是将之前所创建每个连接点的位置计算出运动方向 ,然后让粒子每次运动一段树枝小节的距离(即两个连接点的距离),同时在运动中,让粒子的大小逐渐缩小,从而实现了树枝底端粗,枝端细的效果。粒子运动和渲染代码如下所示。

kotlin 复制代码
class Branch {
    ......
    display(){
        if(! this.finished){
            push();
            noStroke();
            fill(75,87,62,this.ca);
            let idxLen = dist(this.points[this.idx].x,this.points[this.idx].y,this.points[this.idx+1].x,this.points[this.idx+1].y);
            let ratio = dist(this.x,this.y,this.points[this.idx+1].x,this.points[this.idx+1].y) / idxLen;
            let br = ratio*this.sizes[this.idx]+(1-ratio)*this.sizes[this.idx+1];
            circle(this.x,this.y,br*2);

            pop();
        }

    }

    move(){
        // console.log(this.idx);
        if (this.idx == this.num) {
            this.finished = true;
            return true;
        }
        if(dist(this.x,this.y,this.points[this.idx+1].x,this.points[this.idx+1].y) < 5){
            this.idx += 1;
            if (this.idx == this.num) {
                this.finished = true;
                return true;
            }
        }
        let branchDir = [this.points[this.idx+1].x-this.points[this.idx].x,this.points[this.idx+1].y-this.points[this.idx].y];
        branchDir = [branchDir[0]/dist(branchDir[0],branchDir[1],0,0),branchDir[1]/dist(branchDir[0],branchDir[1],0,0)];

        this.x += branchDir[0]*this.v;
        this.y += branchDir[1]*this.v;

        return false;
    }
}

五、绘制蝴蝶结

蝴蝶结主要就是通过贝塞尔曲线 来绘制,由于想要增加艺术感,我使蝴蝶结的贝塞尔曲线以不同的大小来进行叠加,从而呈现出一种油画的感觉。

scss 复制代码
// 蝴蝶结的贝塞尔曲线
var bezierButterflySet = [    [[284.87,311.02],[277.92,303.78],[235.93,315.37],[224.06,314.21],[214.50,313.05],    // 蝴蝶结左半边形状
        [209,299.73],[209.87,295.10],[211.02,291.91],[206.68,280.04],[226.08,272.51],
        [238.54,268.45],[277.92,304.94],[284.87,311.02]],
    [[284.87,311.02],[317.59,302.05],[315.28,294.52],[322.81,284.67],[331.78,275.11],    // 蝴蝶结右半边形状
        [347.13,269.61],[351.19,271.35],[354.66,273.67],[363.64,279.75],[361.90,291.91],
        [360.45,304.07],[348.87,308.71],[339.60,310.15],[332.36,311.31],[317.88,301.76],
        [284.87,311.02]],
    [[284.87,311.02],[284.87,311.02],[253.30,327.75],[242.01,380.75]],    // 蝴蝶结左下角带子
    [[284.87,311.02],[284.87,311.02],[342.21,356.13],[328.89,380.45]]    // 蝴蝶结右下角带子
]

// 绘制蝴蝶结
function drawButterfly() {
    var x_all = 0;  // 蝴蝶结的位置
    var y_all = 0;
    var butterc = fc; // 蝴蝶结的颜色
    var lineDelta = map(flowerNum,8,19,2,5); // 蝴蝶结中心的块

    for(var i = 0;i < branches.length;i++){  // 通过树枝的第三个节点即旋转节点位置来计算蝴蝶结的中心位置
        x_all += branches[i].points[2].x;
        y_all += branches[i].points[2].y;
    }

    x_all /= branches.length;
    y_all /= branches.length;

    for(var i = 0;i < branches.length;i++){
        push();
        butterc.setAlpha(120); // 设置不透明度
        stroke(butterc);
        strokeWeight(3);
        noFill();
        var deltaY = map(i,0,branches.length-1,-5,5)+randomGaussian(0,2);
        line(x_all-lineDelta,y_all + deltaY,x_all+lineDelta,y_all+deltaY); // 绘制蝴蝶结中心的块

        pop();
    }

    push();
    var butternum = 1000; // 叠加数量

    for(var i = 0;i < butternum;i++){
        var s = (butternum-i)/butternum/2; // 计算当前的缩放大小
        var a = constrain(map(i,0,butternum,0,200),0,random(200)); // 计算当前的不透明度
        push();
        // 对蝴蝶结进行缩放
        translate(x_all,y_all);  
        scale(s);
        translate(-x_all,-y_all);

        noFill();
        strokeWeight(1);
        butterc.setAlpha(a);
        stroke(butterc);

        beginShape(); // 开始绘制蝴蝶结的左半边形状
        vertex(x_all,y_all);

        for(var k = 1;k < butterflySet[0].length;k+=3){
            bezierVertex(butterflySet[0][k].x+x_all,butterflySet[0][k].y+y_all,
                         butterflySet[0][k+1].x+x_all,butterflySet[0][k+1].y+y_all,
                        butterflySet[0][k+2].x+x_all,butterflySet[0][k+2].y+y_all)
        }

        endShape();

        beginShape();  // 开始绘制蝴蝶结的右半边形状
        vertex(x_all,y_all);

        for(var k = 1;k < butterflySet[1].length;k+=3){
            bezierVertex(butterflySet[1][k].x+x_all,butterflySet[1][k].y+y_all,
                         butterflySet[1][k+1].x+x_all,butterflySet[1][k+1].y+y_all,
                        butterflySet[1][k+2].x+x_all,butterflySet[1][k+2].y+y_all)
        }

        endShape();

        // 绘制蝴蝶结的左下的带子
        bezier(x_all,y_all,x_all,y_all,butterflySet[2][2].x+x_all,butterflySet[2][2].y+y_all,
            butterflySet[2][3].x+x_all,butterflySet[2][3].y+y_all);

        // 绘制蝴蝶结的右下的带子
        bezier(x_all,y_all,x_all,y_all,butterflySet[3][2].x+x_all,butterflySet[3][2].y+y_all,
            butterflySet[3][3].x+x_all,butterflySet[3][3].y+y_all);
        pop();
    }

}
相关推荐
Maer097 分钟前
Cocos Creator3.x设置动态加载背景图并且循环移动
javascript·typescript
大怪v20 分钟前
前端恶趣味:我吸了juejin首页,好爽!
前端·javascript
反应热35 分钟前
浏览器的本地存储技术:从 `localStorage` 到 `IndexedDB`
前端·javascript
刘杭36 分钟前
在react项目中使用Umi:dva源码简析之redux-saga的封装
前端·javascript·react.js
某公司摸鱼前端39 分钟前
js 如何代码识别Selenium+Webdriver
javascript·selenium·测试工具·js
有一个好名字1 小时前
Vue Props传值
javascript·vue.js·ecmascript
pan_junbiao1 小时前
Vue使用axios二次封装、解决跨域问题
前端·javascript·vue.js
秋沐1 小时前
vue3中使用el-tree的setCheckedKeys方法勾选失效回显问题
前端·javascript·vue.js
南斯拉夫的铁托2 小时前
(PySpark)RDD实验实战——取最大数出现的次数
java·javascript·spark
GoppViper3 小时前
uniapp js修改数组某个下标以外的所有值
开发语言·前端·javascript·前端框架·uni-app·前端开发