[p5.js作品教程] 情人节花束

Bunches of flowers

作品地址及完整代码见openprocessing

视频见 B站

一、背景绘制

  将画布看作是640x640的网格,在每个网格中绘制直径为1的圆形,而每个圆形填充颜色的透明度与该位置y坐标值成正相关,即y值越大,透明度越大。
  将画面中间裁去一个圆圈,在位于圆圈里面的点的位置绘制直径为1的圆形,每个圆形填充颜色的透明度与该位置y坐标值成负相关,即y值越大,透明度越小。
  同时为了让渐变看的更自然,对每个位置填充颜色的透明度加上了randomGaussian(0,10)的高斯噪声。
  代码如下

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();  
}

  效果如下
在这里插入图片描述

  这里有个很大的缺陷,就是每一次绘制背景都需要计算640x640次,故生成时间很慢!!!所以才需要在代码最后加上一个noLoop()防止占用过多进程。

二、花瓣绘制
1. 获取花瓣的贝塞尔曲线起始点和控制点

  工具:PS

  1. 新建画布,大小最好和processing或p5js的画布大小一致;
  2. 使用钢笔工具绘制出想要的花瓣形状;
  3. 将工具换成直接选择工具(快捷键 A),打开信息面板,将鼠标移至曲线控制点或起始点处,记录下信息面板中的x与y值(如下图所示)在这里插入图片描述
    若出现以下情况,即一条曲线(绿色箭头所指曲线)由三个点构成,则将第一个控制点坐标为起始点,即起始点坐标、起始点坐标、第二个控制点坐标、结束点
    在这里插入图片描述
  4. 记录下所有点的坐标,格式为:起始点坐标、第一个控制点坐标、第二个控制点坐标、结束点、另一条曲线的第一个控制点、另一条曲线的第二个控制点、另一条曲线的结束点…如下所示
// 存放了四朵不同形状花瓣的集合
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中的坐标位置移到我们所想要的坐标位置,即将花瓣的所有点坐标(包括起始点和控制点)全部转化为相对于第一个起始点位置的相对坐标(而不是绝对坐标),函数如下所示
// 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;
}

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

// 存放了四朵不同形状花瓣的集合
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属性,来标明这朵花是否是以侧面来面对我们的,此属性可通过减少花瓣的数量和限制花瓣的旋转角度来实现。
具体代码如下所示。

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连接起来,便可实现树枝的绘制,多说无益,可以看看代码来进行理解。

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();
    }

}
五、绘制蝴蝶结

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

// 蝴蝶结的贝塞尔曲线
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();
    }

}
作品展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

作品地址及完整代码见openprocessing

如果有不懂的地方,欢迎评论区留言或在Processing论坛来进行留言,我有时间就会抽空来回答!感谢支持呀!>_<

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hzxwonder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值