PlotClock“小贱钟”彻底研究——模拟器美化、代码重构(番
本篇文章,没有进行新的试验和探索,而是对现有产出物的梳理和优化。
![](https://img.haomeiwen.com/i928040/2d7de4efd2555d9a.png)
经过了几何计算、模拟程序和运笔算法的梳理,我已经拥有了一个可以用于进行各种测试的模拟程序。后续将要使用这个模拟程序进行更多的测试和试验。模拟程序的代码还会持续测增加,为了方便后续进行各种测试。再使用一个大函数包含所有代码,将不利于后续的维护。并且,后续重试不同的算法也需要代码具有一个很好的扩展性,是时候对代码进行结构的优化了。
优化的目标就是将模拟器结构数据、渲染方式、辅助信息、运笔算法都进行单独的封装方便后续替换不同通的模块进行测试。
先展示一下最终的结果,模拟器有了一些仿真的样子,并且转动的角度有和笔的坐标都有了标注。模拟器还支持多种运笔的算法,图中就是一个画圆的算法和一个画直线的算法组合运行的效果。
![](https://img.haomeiwen.com/i928040/657aa85997991f14.gif)
对类的结构先做一个简单的规划,设计大概如下:
![](https://img.haomeiwen.com/i928040/b2ee50dab24f3f1a.png)
其中主体程序是Processing的主体程序主要包含了,setUp和draw两个方法,以及一些全局变量的定义。
PlotClock结构
主要负责保存摆臂长度以及舵机的位置这些信息,并且负责转动左右一级摆臂来控制笔的位置。其中也包含了根据笔的坐标,计算两个一级摆臂的角度的方法。
为了装饰器能够正确的装饰,PlotClock结构类提供获取摆臂长度、持笔夹长度、左舵机转轴位置,左右舵机转轴距离,左右一级摆臂与X轴夹角的度数。有了这些数据装饰器就能够进行正确的渲染。
为了运笔算法能够操作PlotClock结构,提供一个转动摆臂的方法,方法接受左右摆臂的角度。同时提供根据笔的绘制点坐标计算出左右一级摆臂与X轴夹角的方法。
装饰器
主要负责根据PlotClock结构的数据来进行装饰渲染,如描绘左右摆臂的角度,描绘仿真的摆臂样式等,根据需求可以增加不同的装饰器。
运笔控制器
负责为PlotClock结构提供每一次循环需要转动的角度。而这个角度的计算根据不同的运笔控制算法而不同。要实现不同的绘制方法可以增加不同的运笔控制器。
个模块如何协同工作
可以看一下主体控制程序,经过重构之后精简了很多:
void setup(){
size(600,600);
//origin_x,origin_y,distance,L1,L2,L3,bata
pcs = new PlotClockStructure(250,500,100,150,190,90,135);
decorateList = new ArrayList<Decorate>();
//加入X轴装饰器
decorateList.add(new XAxis());
//加入角度装饰器,显示左右一级半壁与X轴的夹角
decorateList.add(new AngleDecorater());
//加入书写点坐标装饰器
decorateList.add(new PenCoordinate());
//加入书写点装饰器,书写点将显示为一个红点
decorateList.add(new PenPointDecorater());
//加入书写轨迹装饰器,显示书写的轨迹
decorateList.add(new PenPathDecorater());
//加入模拟器装饰器,显示摆臂模拟结构
decorateList.add(new Simulation());
size(WIDTH,HEIGHT);
controllerList = new ArrayList<Controller>();
//创建线段运笔控制器,并加入列表
Controller controller1 = new LineController();
controller1.init(new Point(0,190),new Point(150,190),pcs);
controllerList.add(controller1);
//创建圆运笔控制器,并加入列表
Controller controller2 = new Circle();
controller2.init(new Point(50,300),new Point(150,300),pcs);
controllerList.add(controller2);
//初始化运笔控制器的数量以及循环控制索引
controllerCount = controllerList.size();
controllerIndex = 0;
isNewController = true;
}
void draw(){
if(controllerIndex >= controllerList.size()){
noLoop();
return;
}
background(255);
//获取当前的运笔控制器
Controller con = controllerList.get(controllerIndex);
//如果是第一次使用列表中的运笔控制器,测获取初始化的角度,并将是否第一次使用的标志设为false
if(isNewController){
alpha = controllerList.get(controllerIndex).getAlpha();
theta = controllerList.get(controllerIndex).getTheta();
isNewController = false;
}
//根据角度转动一级摆臂
pcs.trunL1(alpha,theta);
//转动完成之后,运行所有添加的装饰器
for(Decorate decorate:decorateList){
decorate.decorate();
}
stroke(0,0,0);
//如果运笔控制器需要转动到下一个角度,则获取这个角度
//否则运笔控制器索引增加1
if(con.hasNext()){
alpha = con.getAlpha();
theta = con.getTheta();
}else{
controllerIndex += 1;
}
}
通过以上的代码就可以将结构、装饰器、运笔控制器整合在一起协同工作。
装饰器
除了模拟装饰器,其他的装饰器都比较简单。这里分享一下模拟装饰器的代码。
class Simulation extends Decorate{
final float RADIUS = 20;
final float HALF_GAP_ANGLE = 30;
void decorate(){
stroke(0,0,0);
noFill();
renderL(pcs.getOriginX(),pcs.getOriginY(),pcs.getLeftL1EndX(),pcs.getLeftL1EndY());
renderL(pcs.getLeftL1EndX(),pcs.getLeftL1EndY(),pcs.getLeftL2EndX(),pcs.getLeftL2EndY());
renderL(pcs.getOriginX()+pcs.getDistance(),pcs.getOriginY(),pcs.getRightL1EndX(),pcs.getRightL1EndY());
renderL(pcs.getRightL1EndX(),pcs.getRightL1EndY(),pcs.getLeftL2EndX(),pcs.getLeftL2EndY());
renderClip(pcs.getLeftL2EndX(),pcs.getLeftL2EndY(),pcs.getLeftL3EndX(),pcs.getLeftL3EndY());
}
float angle(float begin_x,float begin_y,float end_x,float end_y){
if(begin_y==end_y){
if(begin_x>end_x){
return PI;
}else{
return 0;
}
}
float angle = 0;
if(end_y>begin_y){
angle = asin((end_y-begin_y)/sqrt(sq(begin_y-end_y)+sq(begin_x-end_x)));
if(end_x>begin_x){
}else{
angle=PI-angle;
}
}else{
angle = asin((begin_y-end_y)/sqrt(sq(begin_y-end_y)+sq(begin_x-end_x)));
if(end_x<begin_x){
angle = angle+PI;
}else{
angle = TWO_PI-angle;
}
}
return angle;
}
void renderL(float begin_x,float begin_y,float end_x,float end_y){
float begin_angle = angle(begin_x,begin_y,end_x,end_y);
arc(begin_x,begin_y,2*RADIUS,2*RADIUS,begin_angle-TWO_PI+radians(HALF_GAP_ANGLE),begin_angle-radians(HALF_GAP_ANGLE));
ellipse(begin_x,begin_y,15,15);
float end_angle = angle(end_x,end_y,begin_x,begin_y);
arc(end_x,end_y,2*RADIUS,2*RADIUS,end_angle-TWO_PI+radians(HALF_GAP_ANGLE),end_angle-radians(HALF_GAP_ANGLE));
ellipse(end_x,end_y,15,15);
float left_y = -RADIUS*sin(begin_angle-radians(HALF_GAP_ANGLE));
float left_x = RADIUS*cos(begin_angle-radians(HALF_GAP_ANGLE));
float left_e_y = -RADIUS*sin(PI+end_angle+radians(HALF_GAP_ANGLE));
float left_e_x = RADIUS*cos(PI+end_angle+radians(HALF_GAP_ANGLE));
line(left_x+begin_x,begin_y-left_y,end_x-left_e_x,end_y+left_e_y);
float right_y = -RADIUS*sin(begin_angle+radians(HALF_GAP_ANGLE));
float right_x = RADIUS*cos(begin_angle+radians(HALF_GAP_ANGLE));
float right_e_y = -RADIUS*sin(PI+end_angle-radians(HALF_GAP_ANGLE));
float right_e_x = RADIUS*cos(PI+end_angle-radians(HALF_GAP_ANGLE));
line(right_x+begin_x,begin_y-right_y,end_x-right_e_x,end_y+right_e_y);
}
void renderClip(float begin_x,float begin_y,float end_x,float end_y){
float begin_angle = angle(begin_x,begin_y,end_x,end_y);
arc(begin_x,begin_y,2*RADIUS,2*RADIUS,begin_angle-TWO_PI+radians(HALF_GAP_ANGLE),begin_angle-radians(HALF_GAP_ANGLE));
float end_angle = angle(end_x,end_y,begin_x,begin_y);
arc(end_x,end_y,3*RADIUS,3*RADIUS,end_angle-TWO_PI+radians(HALF_GAP_ANGLE)-PI,end_angle-radians(HALF_GAP_ANGLE)-PI);
arc(end_x,end_y,2*RADIUS,2*RADIUS,end_angle-TWO_PI+radians(HALF_GAP_ANGLE)-PI,end_angle-radians(HALF_GAP_ANGLE)-PI);
line(1.5*RADIUS*cos(end_angle-TWO_PI+radians(HALF_GAP_ANGLE)-PI)+end_x,1.5*RADIUS*sin(end_angle-TWO_PI+radians(HALF_GAP_ANGLE)-PI)+end_y,
RADIUS*cos(end_angle-TWO_PI+radians(HALF_GAP_ANGLE)-PI)+end_x,RADIUS*sin(end_angle-TWO_PI+radians(HALF_GAP_ANGLE)-PI)+end_y);
line(1.5*RADIUS*cos(end_angle-radians(HALF_GAP_ANGLE)-PI)+end_x,1.5*RADIUS*sin(end_angle-radians(HALF_GAP_ANGLE)-PI)+end_y,
1*RADIUS*cos(end_angle-radians(HALF_GAP_ANGLE)-PI)+end_x,1*RADIUS*sin(end_angle-radians(HALF_GAP_ANGLE)-PI)+end_y);
float left_y = -RADIUS*sin(begin_angle-radians(HALF_GAP_ANGLE));
float left_x = RADIUS*cos(begin_angle-radians(HALF_GAP_ANGLE));
float left_e_y = -RADIUS*sin(PI+end_angle+radians(HALF_GAP_ANGLE));
float left_e_x = RADIUS*cos(PI+end_angle+radians(HALF_GAP_ANGLE));
line(left_x+begin_x,begin_y-left_y,end_x-left_e_x,end_y+left_e_y);
float right_y = -RADIUS*sin(begin_angle+radians(HALF_GAP_ANGLE));
float right_x = RADIUS*cos(begin_angle+radians(HALF_GAP_ANGLE));
float right_e_y = -RADIUS*sin(PI+end_angle-radians(HALF_GAP_ANGLE));
float right_e_x = RADIUS*cos(PI+end_angle-radians(HALF_GAP_ANGLE));
line(right_x+begin_x,begin_y-right_y,end_x-right_e_x,end_y+right_e_y);
}
}
一开始的思路就是获取每个摆臂的坐标然后进行绘制。做了两个摆臂之后觉得实在是太繁琐了,要不停的进行坐标转换。同事,j还要进行三角函数的计算,计算是还需要按照坐标的情况进行转换。因为用sin计算时,如果结果是0需要区分到底是0度还是180度或者360度。使用cos时也有这样的问题。并且每个端点都要做坐标转换,才能进行三角计算,最终放弃了这样的方法。
通过分析,最终使用了向量法。通过起点和终点生成一个向量,然后计算角度,计算角度时也统一角度范围为0到360度。如下图所示:
![](https://img.haomeiwen.com/i928040/0b2a225f278ec8a8.png)
代码中的angle方法可以把向量的角度在0到360度之间的值返回。
有了这个通用的方法,再把每一个摆臂当做一个向量,就可以方便的计算每一个摆臂的角度,并根据角度进行对应的渲染。
运笔控制器
运笔控制算法通过根据不同的算法将所需绘制的轨迹分解为小的步数,然后计算每步对应需要转动的角度。最终完成不同轨迹的绘制。
interface Controller{
void init(Point begin,Point end,PlotClockStructure pcs);
float getAlpha();
float getTheta();
boolean hasNext();
}
class LineController implements Controller{
int count,counter;
float alpha,theta,tx,ty,x_step,y_step;
void init(Point begin,Point end,PlotClockStructure pcs){
counter = 0;
tx = begin.getX();
ty = begin.getY();
float length_x = abs(begin.getX()-end.getX());
float length_y = abs(begin.getY()-end.getY());
if(length_x > length_y){
count = round(abs(begin.getX()-end.getX()));
x_step = length_x/count;
y_step = length_y/count;
}else{
count = round(abs(begin.getY()-end.getY()));
}
alpha = pcs.point2alpha(begin.getX(),begin.getY());
theta = pcs.point2theta(end.getX(),end.getY());
}
float getAlpha(){
return alpha;
}
float getTheta(){
return theta;
}
boolean hasNext(){
if(counter<count){
tx += x_step;
ty += y_step;
alpha = degrees(pcs.point2alpha(tx,ty));
theta = degrees(pcs.point2theta(tx, ty));
counter++;
return true;
}else{
return false;
}
}
}
class Circle implements Controller{
int count,counter;
float alpha,theta,tx,ty,x_step,y_step,radius;
Point center;
void init(Point begin,Point end,PlotClockStructure pcs){
count = 360;
counter = 0;
center = begin;
radius = sqrt(sq(begin.getX()-end.getX())+sq(begin.getY()-end.getY()));
alpha = pcs.point2alpha(begin.getX()+radius,begin.getY());
theta = pcs.point2theta(end.getX()+radius,end.getY());
}
float getAlpha(){
return alpha;
}
float getTheta(){
return theta;
}
boolean hasNext(){
if(counter<count){
tx=radius*cos(radians(counter))+center.getX();
ty=radius*sin(radians(counter))+center.getY();
alpha = degrees(pcs.point2alpha(tx,ty));
theta = degrees(pcs.point2theta(tx, ty));
counter++;
return true;
}else{
return false;
}
}
}
有了以上的代码重构,后续就可以添加更多的运笔控制器来绘制不同的图形了。
以上就是整体的重构情况,有了这个基础,方便后续做更多不同方向的尝试和试验。