p5.js-创意绘画系统

一、前言

既动态码绘之后,我们又来尝试动态绘画系统了。

最后的实验作业在前面的基础上,进行了规整和扩展,功能更加丰富,操作交互性更强,形成了一个完整的系统。整个绘画系统具有模板动态画刷、模拟毛笔、模拟刷子、滤镜等功能,可选择性也比较强。另外还增加了部分动态效果,使得整个系统更加有趣。

二、实验内容

(一)主题

编写一个“绘画系统”,提供一系列绘画材料(例如画笔/颜料/滤镜)给用户操作,以创作出动态/交互的绘画作品。这个绘画系统是对“绘画”的概念的扩展,但仍然体现出与传统绘画系统的相似性。

(二)操作说明

1、界面介绍

界面介绍

2、功能介绍

(1)点击“颜色”区域更改画笔颜色,也可以用键盘 “↑” “↓” 键更改画笔颜色。
(2)点击“动态画刷模板”按钮更换模板样式,有“圆圈”和“星星”两种模板可供选择;也可以用“空格”键进行此操作。
(3)点击“模拟画笔”按钮更换画笔样式,有“毛笔”和“刷子”两种模式可供选择;也可以用“T”键进行此操作。
(4)点击“橡皮擦”按钮,选择橡皮擦工具;也可以用“E”键进行此操作。
(5)点击“背景模式”按钮更换背景,有“量”和“暗”两种模式可供选择;也可以用“B”键进行此操作。
(6)点击“播放/暂停”按钮,选择动态或静态模式;也可以用“G”键进行此操作。
(7)点击“清空”按钮,清空当前屏幕上所有画笔痕迹;也可以用“C”键进行此操作。
(8)点击“打开图片”按钮,加载一张图片并显示在窗口中,再次点击将图片移除;也可以用“O”键进行此操作。
(9)点击“保存图片”按钮,将当前画布内保存为png图片;也可以用“S”键进行此操作。
(10)点击“滤镜”按钮,为当前画布添加滤镜,再次点击滤镜会撤销;也可以用“L”键进行此操作。

(三)效果展示

1、动态画刷模板
  • 静态:
    在这里插入图片描述
  • 动态:在这里插入图片描述
  • 趣味小交互

按住鼠标右键,画笔模板会躲避鼠标。在动态静态下都有效。
在这里插入图片描述

2、模拟画笔

在绘制过程中,画笔移动速度越快,墨迹越细,颜色越浅。 当画笔移动速度很小时,还会模拟溅出墨点

  • 模拟毛笔在这里插入图片描述

  • 模拟刷子(焦墨)
    在这里插入图片描述

  • 动态效果
    当打开动态效果时,墨迹会随着时间逐渐变浅,墨色越浅的地方变浅的速度也越快。
    在这里插入图片描述

3、背景变化

在这里插入图片描述

4、橡皮擦

在这里插入图片描述

5、滤镜

打开图片:在这里插入图片描述
添加滤镜:
在这里插入图片描述
滤镜效果时先对图像用均值滤波进行了平滑处理,又对色彩进行了分级,并且适当增加了色彩的饱和度。

三、程序详解

(一)程序结构

······ 一些变量的设置
function setup(){       //···初始化窗口
  ···设置画布
  ···加载图片
  ···菜单按钮创建初始化  
}
function draw(){         //···窗口实时绘制
  ···计时更新
  ···根据菜单按键状态在窗口显示相应操作
}

function FuncBtn(X, Y, W, H, CMD){}  //功能函数,用于创建功能按钮。下面几个为该函数的子函数,定义相应操作
FuncBtn.prototype.isMouseInBtn = function(){}  //当鼠标在功能键内的状态
FuncBtn.prototype.clickBtn = function(){}      //点击功能按钮时的响应
FuncBtn.prototype.displayBtn = function(){}    //功能按钮显示在窗口

function ColorBtn(X, Y, W, H, givenR, givenG, givenB){}  //颜色按钮函数,用于创建颜色按钮。下面几个为该函数的子函数,定义相应操作
ColorBtn.prototype.isMouseInBtn = function(){}           //当鼠标在功能键内的状态
ColorBtn.prototype.clickBtn = function(){}               //点击颜色按钮时的响应
ColorBtn.prototype.displayBtn = function(){}             //颜色按钮显示在窗口

function Node(position, givenSize, givenR, givenG, givenB){}  //墨迹单元函数,用来创建单个墨迹。下面几个为该函数的子函数,定义相应操作
Node.prototype.drawing = function(){}                         //将墨迹绘制在窗口中
Node.prototype.update = function(){}                          //动态状态下墨迹更新

function mouseClicked(){}    //鼠标响应函数
function keyPressed(){}      //键盘响应函数

系统流程图:
在这里插入图片描述

(二)一些重要的代码

1、重要变量
  • var objs = []; var btns = [];
    按钮数组和画笔单元数组
  • var FPS = 60;var timepast = 0;
    动态计时相关变量
  • var brushType = “INK2”;var pbrushType = “INK2”;
    画笔单元种类,在画笔单元创建时进行修改,在重绘时使用
  • var isPlaying = false;var isOpen=false;var isLv=false;var isMenuHide= false;
    这几个boolen变量,分别用来控制静态/动态、是否显示图片、是否添加滤镜效果、菜单是否隐藏
  • var colornum=13;
    颜色标记,表示当前颜色序号,在点击颜色按钮时修改,在键盘响应中使用
2、绘制星星
translate(this.position.x, this.position.y);    rotate(this.rotateAngle);
fill(this.size.x * this.R / 10, this.size.x * this.G / 10, this.size.x * this.B / 10, round(sin(this.timepast) * 128));    
x0=sin(this.timepast) * this.baseSize.x;    
y0=cos(this.timepast) * this.baseSize.y;    
RR=this.size.x * 1.5;    
var r=RR/2;    
beginShape();    
vertex(x0,y0+RR);  
vertex(x0+r*sin(36.0/180*PI),y0+r*cos(36.0/180*PI));    
vertex(x0+RR*sin(72.0/180*PI),y0+RR*cos(72.0/180*PI));    
vertex(x0+r*cos(18.0/180*PI),y0-r*sin(18.0/180*PI));    
vertex(x0+RR*sin(36.0/180*PI),y0-RR*cos(36.0/180*PI));    
vertex(x0,y0-r);    
vertex(x0-RR*sin(36.0/180*PI),y0-RR*cos(36.0/180*PI));    
vertex(x0-r*cos(18.0/180*PI),y0-r*sin(18.0/180*PI));    
vertex(x0-RR*sin(72.0/180*PI),y0+RR*cos(72.0/180*PI));    
vertex(x0-r*sin(36.0/180*PI),y0+r*cos(36.0/180*PI));    
vertex(x0,y0+RR);    
endShape();

x0、y0为当前星星的中心,RR为凸角顶点到中心的距离,r为凹角顶点到中心的距离。
使用了p5的vertex()函数

3、滤镜
if (isLv){                               //滤镜    
  var d = pixelDensity();                //获取画布内所有像素点    
  var wholeImage = 4 * (canvas.width * d) * (canvas.height * d);        //每个像素点有四个决定值,分别为R、G、B、透明度    
  var peR=1;    
  var peG=1;    
  var peB=1;    
  loadPixels();    
  for (var i = 0; i < wholeImage; i += 4) {                          //均值平滑
    if((((i/4)%canvas.width)<=(canvas.width-2))&&((i/4)<(canvas.width*(canvas.height-2)))){        
      pixels[i]=(pixels[i]+pixels[i+4]+pixels[i+8]
                 +pixels[i+4*canvas.width]+pixels[i+4+4*canvas.width]+pixels[i+8+4*canvas.width]                  
                 +pixels[i+8*canvas.width]+pixels[i+4+8*canvas.width]+pixels[i+8+8*canvas.width])/9;        
      pixels[i+1]=(pixels[i+1]+pixels[i+5]+pixels[i+9]                  
                 +pixels[i+1+4*canvas.width]+pixels[i+5+4*canvas.width]+pixels[i+9+4*canvas.width]                  
                 +pixels[i+1+8*canvas.width]+pixels[i+5+8*canvas.width]+pixels[i+9+8*canvas.width])/9;        
      pixels[i+2]=(pixels[i+2]+pixels[i+6]+pixels[i+10]                  
                 +pixels[i+2+4*canvas.width]+pixels[i+6+4*canvas.width]+pixels[i+10+4*canvas.width]                  
                 +pixels[i+2+8*canvas.width]+pixels[i+6+8*canvas.width]+pixels[i+10+8*canvas.width])/9;             
    }       
  }    
  for (var i = 0; i < wholeImage; i += 4) {                  //色彩分级
    peR=(pixels[i]*3.0)/(pixels[i]+pixels[i+1]+pixels[i+2]);      
    peG=(pixels[i+1]*3.0)/(pixels[i]+pixels[i+1]+pixels[i+2]);      
    peB=(pixels[i+2]*3.0)/(pixels[i]+pixels[i+1]+pixels[i+2]);            
    if ( pixels[i]>=0&& pixels[i]<36){        
      pixels[i]=18;     
    }     
    else if( pixels[i]>=36&& pixels[i]<72){        
      pixels[i]=54;      
    }      
    else if( pixels[i]>=72&& pixels[i]<108){        
      pixels[i]=90;      
    }      
    else if( pixels[i]>=108&& pixels[i]<144){        
      pixels[i]=126;      
    }      
    else if( pixels[i]>=144&& pixels[i]<180){        
      pixels[i]=162;      
    }      
    else if( pixels[i]>=180&& pixels[i]<216){        
      pixels[i]=198;      
    }      
    else{        
      pixels[i]=234;      
    }      
    pixels[i]=pixels[i]*peR;
      
    if ( pixels[i+1]>=0&& pixels[i+1]<36){        
    pixels[i+1]=18;      
    }      
    else if( pixels[i+1]>=36&& pixels[i+1]<72){        
    pixels[i+1]=54;      
    }      
    else if( pixels[i+1]>=72&& pixels[i+1]<108){        
    pixels[i+1]=90;      
    }      
    else if( pixels[i+1]>=108&& pixels[i+1]<144){        
      pixels[i+1]=126;      
    }      
    else if( pixels[i+1]>=144&& pixels[i+1]<180){        
      pixels[i+1]=162;      
    }      
    else if( pixels[i+1]>=180&& pixels[i+1]<216){        
      pixels[i+1]=198;      
    }      
    else{        pixels[i+1]=234;      
    }      
    pixels[i+1]=pixels[i+1]*peG;            
    
    if ( pixels[i+2]>=0&& pixels[i+2]<36){        
      pixels[i+2]=18;      
    }      
    else if( pixels[i+2]>=36&& pixels[i+2]<72){        
      pixels[i+2]=54;      
    }      
    else if( pixels[i+2]>=72&& pixels[i+2]<108){        
      pixels[i+2]=90;      
    }      
    else if( pixels[i+2]>=108&& pixels[i+2]<144){        
      pixels[i+2]=126;      
    }      
    else if( pixels[i+2]>=144&& pixels[i+2]<180){        
      pixels[i+2]=162;      
    }      
    else if( pixels[i+2]>=180&& pixels[i+2]<216){        
      pixels[i+2]=198;      
    }      
    else{        
      pixels[i+2]=234;      
    }        
    pixels[i+2]=pixels[i+2]*peB;        
  }        
  updatePixels();      
}

滤镜效果的实现分为5步:
(1)提取出画布中的所有像素
(2)平滑处理。这里使用的是临近9个像素点的均值。
(3)R、G、B分别进行色彩分级。这里分了7级
(4)饱和度增强

  • 根据每个像素RGB分量的权值来决定哪个颜色增强,哪个颜色减弱,以达到增强饱和度的效果
    peR=(pixels[i]*3.0)/(pixels[i]+pixels[i+1]+pixels[i+2]);
    peG=(pixels[i+1]*3.0)/(pixels[i]+pixels[i+1]+pixels[i+2]);
    peB=(pixels[i+2]*3.0)/(pixels[i]+pixels[i+1]+pixels[i+2]);

(5)更新画布中的所有像素点

这里使用了loadPixels()函数

4、模拟画笔

模拟毛笔:

        var dis=sqrt((this.pmouseX-this.mouseX)*(this.pmouseX-this.mouseX)*1.0+(this.pmouseY-this.mouseY)*(this.pmouseY-this.mouseY)*1.0);    
        var size=35-dis*0.8;    
        var k=0;    
        var tempx=0;    
        var tempy=0;
    
        if(size<=20)    
        {      
          size=20;    
        }           
        ellipse( this.mouseX, this.mouseY, size, size);        
        if(abs(this.pmouseY-this.mouseY)<abs(this.pmouseX-this.mouseX))
        {
          k=((this.pmouseY-this.mouseY)*1.0)/(this.pmouseX-this.mouseX);
          if(this.pmouseX>this.mouseX){
            for(;tempx<abs(this.pmouseX-this.mouseX);tempx+=3)        {
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,255-dis*this.timepast*2);
              ellipse( this.pmouseX-tempx, this.pmouseY-tempx*k, size, size);
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,200-dis*this.timepast*2);
              ellipse( this.pmouseX-tempx, this.pmouseY-tempx*k, size+5, size+5);
            }
          }
          else{
            for(;tempx<abs(this.pmouseX-this.mouseX);tempx+=3)        {
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,255-dis*this.timepast*2);
              ellipse( this.pmouseX+tempx, this.pmouseY+tempx*k, size, size);
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,200-dis*this.timepast*2);
              ellipse( this.pmouseX+tempx, this.pmouseY+tempx*k, size+5, size+5);
            }
          }
          if(dis<10){
            fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,200-dis*this.timepast*2);
            ellipse( this.mouseX+sin(size*7)*25, this.mouseY+abs(sin(size*7))*25, abs(sin(size*13))*15, abs(sin(size*13))*15);
          }
        }
        else{
          k=((this.pmouseX-this.mouseX)*1.0)/(this.pmouseY-this.mouseY);
          if(this.pmouseY<this.mouseY)      {
            for(;tempy<abs(this.pmouseY-this.mouseY);tempy+=3)
            {
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,255-dis*this.timepast*2);
              ellipse( this.pmouseX+tempy*k, this.pmouseY+tempy, size, size);
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,200-dis*this.timepast*2);
              ellipse( this.pmouseX+tempy*k, this.pmouseY+tempy, size+5, size+5);
            }
          }
          else{
            for(;tempy<abs(this.pmouseY-this.mouseY);tempy+=3)        {
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,255-dis*this.timepast*2);
              ellipse( this.pmouseX-tempy*k, this.pmouseY-tempy, size, size);
              fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,200-dis*this.timepast*2);
              ellipse( this.pmouseX-tempy*k, this.pmouseY-tempy, size+5, size+5);
            }
          }
          if(dis<10){
            fill(this.R*1.5+dis*0.5 , this.G*1.5+dis*0.5 , this.B*1.5+dis*0.5 ,200-dis*this.timepast*2);
            ellipse( this.mouseX, this.mouseY+abs(sin(size*7))*20, abs(sin(size*10))*10, abs(sin(size*10))*10);
          }
        }
        resetMatrix();

dis:鼠标当前位置和上一位置的距离,用来衡量鼠标移动速度。
画笔单元的大小随着速度增加递减。画笔的透明度也随着速度增加递减,同时,在动态状态下,还随着时间的增加递减,模拟出墨迹消失的效果。
这样,就可以模拟出接近真实毛笔的效果,移动速度快时笔迹变细、墨色变浅;移动速度慢时笔记较粗、墨色变深,甚至会溅出多余的墨点。同时,每个墨迹单元还有一个透明度略小、直径略大的圆,用来模拟毛笔的晕染效果。

模拟刷子的算法和模拟毛笔类似,只是墨迹单元是由多个大小不一的圆组成,并且没有墨迹晕染效果。

四、总结与扩展

(一)扩展绘画理念

提到“绘画”这个词,大多数人第一时间联想到的,可能是拿着笔(不论是铅笔毛笔油画笔还是马克笔)在纸上画画,如果是设计行业工作者,也可能会进一步联想到数位板、PS等等,但不论是哪种联想,它们本质上都是绘画者用某种工具(笔,鼠标,甚至是手)在某种载体上(纸、画布、屏幕等等)进行创作或者二次加工。那

么提到“扩展绘画”,联想到的会是什么呢?我脑子里的第一个想法是上海世博会展出的动态《清明上河图》,但是细想一下,动态版《清明上河图》没什么参与性,似乎并不能算作“绘画”。回顾一下绘画发展的历史,不难发现,“绘画”这一概念在一次又一次的革新中不断地被扩展着,从最初的宗教绘画、到文艺复兴、到印象派、再到抽象画,绘画的主题变了、内容变了、手法变量、载体变了、材料也变了,绘画这一概念在不断的扩展中范围越来越广,那么我们如今继续扩展绘画的概念要从哪里入手呢?我想绘画整个过程中的任何一个环节的创新都是可以的,作为用代码画画的人,我们最独到的方式就是从展现效果和交互上入手。

这次实验就是以扩展绘画为目的,进行的小尝试。

(二)创意绘画系统与常规绘画系统的比较

上面说到,对于扩展绘画,我主要是从展现效果和交互上入手的。

在画笔的选择上,本系统主要有动态模板笔刷和模拟真实画笔两部分。
选择动态模板画笔后,每个单位时间内会在鼠标位置一定范围内的随机位置产生一个墨迹单元,并且笔迹大小也根据移动速度随机产生,动态状态下,色彩和位置也是实时变化的,给观看者一种很绚烂的感觉,并且有一定的流动感。
然后增加一个闪躲交互,当在该画笔状态下,按住鼠标右键靠近墨迹单元,画面上已有的圆圈/星星会对鼠标进行闪躲,有一种被冲开的感觉。

在模拟真实画笔的制作中,我的意图是尽量模仿毛笔的感觉。考虑一下真实情况下使用的情景,毛笔在压力大的情况下墨迹会粗,压力小使墨迹变细,但是在电脑上操作显然是没有办法模拟压力的,换一个角度,使用毛笔的时候,越用力的时候速度是越慢的,反之运笔速度快的时候,向下的压力也小,所以可以用速度来模拟,用鼠标运动的速度来决定墨迹的粗细。另外,在用淡墨的时候,墨色的深浅与运笔的速度和力度也是有关系的,所以在模拟的时候,也将墨迹的颜色与速度相结合。为了增加真实性,还模拟了墨的晕染效果,并且在墨迹较粗(也就是移动速度较小)的地方模拟了溅出的墨点,最后效果还是比较令人满意的。
之后又增加了一个墨迹选项,模拟焦墨的状态。为了模拟出焦墨的间隙感,每个墨迹单元由多个小单元组成,每个小单元的粗细依然与速度关联,当鼠标移动速度大于一定的值之后,一些小墨迹单元的粗细会变为0,这样出来的效果比较逼真。
最后为了模拟出墨迹在纸上变浅、消失的情形,对墨迹的透明度进行了处理。每个墨迹单元都会有一个时间变量记录从创建开始的时间,随着时间增加,透明度也减小,直到减小到0,在画面上消失。至于透明度减小的速率,与鼠标移动速度有关,真实情况下,墨色越浅消失得越快,所以这里也是一样,移动速度越快,墨迹越细,墨色越浅,消失得也越快。

创意画图系统中增加了模拟水彩滤镜功能,对当前画布上的内容做色彩分级和饱和度对比度调整。

与常规的绘画系统相比,主要是增强了交互性和趣味性,使用者在作画的过程中参与性更强。动态的展示效果也更加直观、更加有趣。

目前创意绘画系统的功能还比较少,墨迹选项也不是很多,而且当画布中墨迹太多时,运行速度明显降低,对于功能的扩充和算法运行速率的提升还有很大的空间。

参考文件

p5.js官方文档(内附详细函数示例)
p5.js官方示例
p5.js可以做什么 之 创意动态绘图板
图像处理之水彩画特效生成算法
什么是绘画?——以抽象思维理解绘画
西方美术史_百度百科
绘画_百度百科

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值