用创意编程打造动态画板
如果你已经厌倦了用传统方式创作图形作品。不妨换个思路,用创意编程给自己制作一些有趣的绘画工具
比如,你可以用它绘制流动的星空
https://v.qq.com/txp/iframe/player.html?vid=b0172gz439i&width=500&height=375&auto=0
制作会弹跳的图形动画
https://v.qq.com/txp/iframe/player.html?vid=u0172r5mbu3&width=500&height=375&auto=0
也可以写一个旋转画板,制作有趣的图案
https://v.qq.com/txp/iframe/player.html?vid=h0172j7oayd&width=500&height=375&auto=0
以上的演示都是使用Processing 和 openFrameWorks 完成的,它们的原理并不复杂。在以后的 Creative Drawing 系列,会用代码给大家演示,如何在Processing 中实现这些绘图效果。
Creative Drawing
数字和图形,是最让人着迷的组合。抽象的数字一旦和图形发生化学反应,会焕发出新的光彩。
此系列默认大家已经有一定的 Processing 基础,不讲具体的技术细节,通篇围绕“画”来做文章。你可以跟随一个个小实例,由简至繁地深入。
关于画板你要了解的“元知识”
如何存储笔刷的轨迹
如何记录绘制的过程
如何构建笔刷
掌握这些元知识,你就能自如地创作画板了。这篇文章中,主要涉及前两点。
从后面开始,会展示大段的源码,最终你可以实现上例的旋转画板。如果你是没有编程基础的读者,但又希望体验一番,那可以先下载Processing,然后复制实例7的代码。在程序中点击左上角的运行按钮即可。
实例1-最简单的绘图程序
void setup(){
size(500,500);
background(0);
}
void draw(){
}
void mouseDragged(){
stroke(255);
line(pmouseX,pmouseY,mouseX,mouseY);
}
利用拖动事件实现绘制效果。轨迹此时画在了background上,数据并没有以坐标的形式存储下来。
实例2-记录绘制轨迹
PVector []pos;
void setup(){
size(500,500);
pos = new PVector[0];
}
void draw(){
background(0);
stroke(255);
for(int i = 0;i < pos.length - 1;i++){
line(pos[i].x,pos[i].y,pos[i + 1].x,pos[i + 1].y);
}
}
void mouseDragged(){
pos = (PVector [])append(pos,new PVector(mouseX,mouseY)); //每新增一个位置坐标,添加到pos列表后
}
利用 PVector 数组保存系列的坐标。在 draw 函数中,通过循环绘制所有线条
实例3-记录绘制过程
PVector []pos;
float []pTime;
boolean drawOnOff;
float pressTime;
int brushNum;
void setup(){
size(500,500);
background(0);
pos = new PVector[0];
pTime = new float[0];
drawOnOff = false;
brushNum = 0;
}
void draw(){
if(drawOnOff){
if(millis()-pressTime > pTime[brushNum+1]-pTime[0]){
line(pos[brushNum].x + 20,pos[brushNum].y,pos[brushNum+1].x + 20,pos[brushNum+1].y);
brushNum++;
}
if(brushNum+1 == pos.length){ //终止绘制的判断
drawOnOff = false;
}
}
}
void mouseDragged(){
stroke(255);
line(pmouseX,pmouseY,mouseX,mouseY);
pos = (PVector [])append(pos,new PVector(mouseX,mouseY)); //每新增一个位置坐标,添加到pos列表后
pTime = append(pTime,millis());
}
void keyPressed(){
if(keyCode == 'A'){
pressTime = millis();
drawOnOff = true;
}
}
用 pTime 数组储存绘制时间。通过循环,判断与第一个坐标点的时间差,来确定绘制的时间
按 A 键播放
实例4-记录绘制过程(可停顿,可清除重绘)
PVector []pos;
float []pTime;
boolean drawOnOff;
float pressTime;
int brushNum;
void setup(){
size(500,500);
background(0);
pos = new PVector[0];
pTime = new float[0];
drawOnOff = false;
brushNum = 0;
}
void draw(){
if(drawOnOff){
if(millis() - pressTime > pTime[brushNum+1] - pTime[0]){
line(pos[brushNum].x + 20,pos[brushNum].y,pos[brushNum+1].x + 20,pos[brushNum+1].y);
brushNum++;
if(pos[brushNum+1].x == 0 && pos[brushNum+1].y == 0){
brushNum+=2;
}
}
if(brushNum+1 >= pos.length-1){ //终止绘制的判断
drawOnOff = false;
}
}
}
void mouseDragged(){
stroke(255);
line(pmouseX,pmouseY,mouseX,mouseY);
pos = (PVector [])append(pos,new PVector(mouseX,mouseY)); //每新增一个位置坐标,添加到pos列表后
pTime = append(pTime,millis());
}
void mouseReleased(){
pos = (PVector [])append(pos,new PVector(0,0));
pTime = append(pTime,millis());
}
void keyPressed(){
if(keyCode == 'A'){
pressTime = millis();
drawOnOff = true;
}
if(keyCode == 'C'){
background(0);
pos = new PVector[0];
pTime = new float[0];
brushNum = 0;
}
}
实例3只能记录连续的绘制过程。若想区分不同的笔画,可在 mouseReleased 事件激活时,用坐标(0,0)来表示间隔
按 C 键重绘
实例5-旋转画板
PVector []pos;
PVector [][]newPos;
float []pTime;
boolean drawOnOff;
float pressTime;
int brushNum,num;
void setup(){
size(500,500);
background(0);
num = 6; //设置旋转的画笔数量
pos = new PVector[0];
newPos = new PVector[num][0];
pTime = new float[0];
drawOnOff = false;
brushNum = 0;
}
void draw(){
if(drawOnOff){
if(millis() - pressTime > pTime[brushNum+1] - pTime[0]){
for(int i = 0;i < num - 1;i++){
line(newPos[i][brushNum].x,newPos[i][brushNum].y,newPos[i][brushNum+1].x,newPos[i][brushNum+1].y);
}
brushNum++;
if(newPos[0][brushNum+1].x == 0 && newPos[0][brushNum+1].y == 0){
brushNum+=2;
}
}
if(brushNum+1 >= newPos[0].length-1){ //终止绘制的判断
drawOnOff = false;
}
}
}
void mouseDragged(){
stroke(255);
line(pmouseX,pmouseY,mouseX,mouseY);
pos = (PVector [])append(pos,new PVector(mouseX,mouseY)); //每新增一个位置坐标,添加到pos列表后
for(int i = 0;i < num - 1;i++){
newPos[i] = (PVector [])append(newPos[i],Trans(pos[pos.length-1],2*PI/num*(i+1)));
}
pTime = append(pTime,millis());
}
void mouseReleased(){
pos = (PVector [])append(pos,new PVector(0,0));
for(int i = 0;i < num;i++){
newPos[i] = (PVector [])append(newPos[i],new PVector(0,0));
}
pTime = append(pTime,millis());
}
void keyPressed(){
if(keyCode == 'A' && pos.length!=0){
pressTime = millis();
drawOnOff = true;
pos = new PVector[0];
}
if(keyCode == 'C'){
background(0);
pos = new PVector[0];
newPos = new PVector[num][0];
pTime = new float[0];
brushNum = 0;
drawOnOff = false;
}
}
PVector Trans(PVector a,float angle){
PVector center = new PVector(width/2,height/2);
float l = PVector.dist(a,center);
float angle1 = atan2(a.y - center.y,a.x - center.x);
float angle2 = angle1 + angle;
float x = center.x + l*cos(angle2);
float y = center.y + l*sin(angle2);
PVector newPos = new PVector(x,y);
return newPos;
}
函数 Trans 计算旋转后的点坐标
实例6-连线规则
PVector []pos;
float brushInterval,connectInterval;
void setup(){
size(650,650);
background(255);
pos = new PVector[0];
brushInterval = 2; // 录入坐标点的距离
connectInterval = 200; //坐标点连线的最大距离
}
void draw(){
}
void mouseDragged(){
if(pos.length == 0 || dist(mouseX,mouseY,pos[pos.length-1].x,pos[pos.length-1].y) > brushInterval){
pos = (PVector [])append(pos,new PVector(mouseX,mouseY));
}
for(int i = 0;i < pos.length;i++){
float r = dist(pos[i].x,pos[i].y,pos[pos.length-1].x,pos[pos.length-1].y);
if(r < connectInterval){
stroke(0,map(r,0,connectInterval,10,5));
line(pos[i].x,pos[i].y,pos[pos.length-1].x,pos[pos.length-1].y);
}
}
}
void keyPressed(){
if(keyCode == 'S'){
saveFrame(frameCount + ".png");
}
}
通过判断坐标点之间的距离,决定连线与否。从而产生粘稠的,层次丰富的网状笔刷
按‘S’ 键保存
实例7 - 网状旋转画板
PVector []pos;
PVector [][]newPos;
float []pTime;
boolean drawOnOff;
float pressTime;
int brushNum,num;
float interval,cInterval;
void setup(){
size(500,500);
background(255);
num = 10;
pos = new PVector[0];
newPos = new PVector[num][0];
pTime = new float[0];
drawOnOff = false;
brushNum = 0;
interval = 5;
cInterval = 100;
}
void draw(){
if(drawOnOff){
if(millis() - pressTime > pTime[brushNum+1] - pTime[0]){
for(int i = 0;i < num-1;i++){
for(int j = 0;j < brushNum;j++){
float r = dist(newPos[i][j].x,newPos[i][j].y,newPos[i][brushNum].x,newPos[i][brushNum].y);
if(r < cInterval){
stroke(0,map(r,0,cInterval,8,2));
line(newPos[i][j].x,newPos[i][j].y,newPos[i][brushNum].x,newPos[i][brushNum].y);
}
}
}
brushNum++;
if(newPos[0][brushNum+1].x == 0 && newPos[0][brushNum+1].y == 0){
brushNum+=2;
}
}
if(brushNum+1 >= newPos[0].length-1){ //终止绘制的判断
drawOnOff = false;
}
}
}
void mouseDragged(){
if(pos.length == 0 || dist(mouseX,mouseY,pos[pos.length-1].x,pos[pos.length-1].y) > interval){
pos = (PVector [])append(pos,new PVector(mouseX,mouseY));
}
println(pos.length);
for(int i = 0;i < pos.length;i++){
float r = dist(pos[i].x,pos[i].y,pos[pos.length-1].x,pos[pos.length-1].y);
if(r < cInterval){
stroke(0,map(r,0,cInterval,8,2));
line(pos[i].x,pos[i].y,pos[pos.length-1].x,pos[pos.length-1].y);
}
}
for(int i = 0;i < num;i++){
newPos[i] = (PVector [])append(newPos[i],Trans(pos[pos.length-1],2*PI/num*(i+1)));
}
pTime = append(pTime,millis());
}
void mouseReleased(){
pos = (PVector [])append(pos,new PVector(0,0));
for(int i = 0;i < num;i++){
newPos[i] = (PVector [])append(newPos[i],new PVector(0,0));
}
pTime = append(pTime,millis());
}
void keyPressed(){
if(keyCode == 'A' && pos.length!=0){
pressTime = millis();
drawOnOff = true;
pos = new PVector[0];
}
if(keyCode == 'C'){
background(255);
pos = new PVector[0];
newPos = new PVector[num][0];
pTime = new float[0];
brushNum = 0;
drawOnOff = false;
}
}
PVector Trans(PVector a,float angle){
PVector center = new PVector(width/2,height/2);
float l = PVector.dist(a,center);
float angle1 = atan2(a.y - center.y,a.x - center.x);
float angle2 = angle1 + angle;
float x = center.x + l*cos(angle2);
float y = center.y + l*sin(angle2);
PVector newPos = new PVector(x,y);
return newPos;
}
按 A 键播放,C 键重绘
将连线规则附加到旋转画板中,大功告成~~
有了画板工具,要创作这类图案就会变得十分便捷。还犹豫什么?拿起画笔,尽情挥洒创意吧~
END
我们下期再见