iOS之OpenGL ES实现手写“绘画板”

设置绘画板

一、设置画板视图
  • 设置绘画板位置、大小、背景
	CGFloat ratio = self.view.frame.size.height / self.view.frame.size.width;
    CGFloat width = 1500;
    CGSize textureSize = CGSizeMake(width, width * ratio);
    UIImage *image = [UIImage imageNamed:@"paper.jpg"];
    self.paintView = [[YDWPaintView alloc] initWithFrame:self.view.bounds
                                            textureSize:textureSize
                                        backgroundImage:image];
    self.paintView.delegate = self;
    [self.view addSubview:self.paintView];
  • 绘画板的初始化
	// 手指按住屏幕,到离开,产生的所有的点
	@property (nonatomic, strong) NSMutableArray *pointsPreDraw;
	// 操作的栈
	@property (nonatomic, strong) YDWPaintStack *operationStack;
	// 撤销的操作的栈
	@property (nonatomic, strong) YDWPaintStack *undoOperationStack;

	self.operationStack = [[YDWPaintStack alloc] init];
    self.undoOperationStack = [[YDWPaintStack alloc] init];
    self.pointsPreDraw = [[NSMutableArray alloc] init];
    
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.context];
    
    self.vertices = malloc(sizeof(Vertex) * 4);
    self.vertices[0] = (Vertex){{-1, 1, 0}, {0, 1}};
    self.vertices[1] = (Vertex){{-1, -1, 0}, {0, 0}};
    self.vertices[2] = (Vertex){{1, 1, 0}, {1, 1}};
    self.vertices[3] = (Vertex){{1, -1, 0}, {1, 0}};
    
    [self setupGLLayer];
    [self genProgram];
    [self genBuffers];
    [self bindRenderLayer:self.glLayer];
    
    // 没有指定纹理尺寸,设置默认值
    if (CGSizeEqualToSize(self.textureSize, CGSizeZero)) {
        self.textureSize = CGSizeMake(self.drawableWidth, self.drawableHeight);
    }
    
    self.paintTexture = [[YDWPaintTexture alloc] initWithContext:self.context
                                                           size:self.textureSize
                                                backgroundColor:self.backgroundColor
                                                backgroundImage:self.backgroundImage];
    [self bindTexture];
    
    self.brushSize = kDefaultBrushSize;
    self.brushColor = [UIColor blackColor];
    self.brushMode = GLPaintViewBrushModePaint;
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self clear];
    });
  • 创建 program
	self.program = [YDWShaderHelper programWithShaderName:@"normal"];
  • 创建 buffer
	glGenFramebuffers(1, &_frameBuffer);
    glGenRenderbuffers(1, &_renderBuffer);
    glGenBuffers(1, &_vertexBuffer);
  • 创建输出层
	CAEAGLLayer *layer = [[CAEAGLLayer alloc] init];
    layer.frame = self.bounds;
    layer.contentsScale = [[UIScreen mainScreen] scale];
    self.glLayer = layer;
    
    [self.layer addSublayer:self.glLayer];
  • 绑定图像要输出的 layer
	glBindRenderbuffer(GL_RENDERBUFFER, self.renderBuffer);
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
    
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER,
                              self.renderBuffer);
  • 绑定要绘制的纹理
	glUseProgram(self.program);
    GLuint textureSlot = glGetUniformLocation(self.program, "Texture");
    
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, self.paintTexture.textureID);
    glUniform1i(textureSlot, 1);
  • 绘制数据到屏幕
	[self.paintTexture drawPoints:points];
    [self display];
  • 绘制
	glDisable(GL_BLEND);
    
    glViewport(0, 0, [self drawableWidth], [self drawableHeight]); // 绘制前先切换 Viewport
    
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    
    glUseProgram(self.program);
    
    GLuint positionSlot = glGetAttribLocation(self.program, "Position");
    GLuint textureCoordsSlot = glGetAttribLocation(self.program, "TextureCoords");
    
    glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
    GLsizeiptr bufferSizeBytes = sizeof(Vertex) * 4;
    glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_DYNAMIC_DRAW);
    
    glEnableVertexAttribArray(positionSlot);
    glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, positionCoord));
    
    glEnableVertexAttribArray(textureCoordsSlot);
    glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, textureCoord));
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    [self.context presentRenderbuffer:GL_RENDERBUFFER];
  • 获取渲染缓存宽度和获取渲染缓存高度
// 获取渲染缓存宽度
- (GLint)drawableWidth {
    GLint backingWidth;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
    
    return backingWidth;
}

// 获取渲染缓存高度
 - (GLint)drawableHeight {
    GLint backingHeight;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
    
    return backingHeight;
}
二、 设置画板纹理
  • 创建着色器程序
// 创建 brushProgram
- (void)genBrushProgram {
    self.brushProgram = [YDWShaderHelper programWithShaderName:@"brush"];
}

// 创建 normalProgram
 - (void)genNormalProgram {
    self.normalProgram = [YDWShaderHelper programWithShaderName:@"normal"];
}
  • 创建缓存和创建 buffer
- (void)genBuffers {
    glGenFramebuffers(1, &_frameBuffer);
    glGenRenderbuffers(1, &_renderBuffer);
    glGenBuffers(1, &_vertexBuffer);
}

// 初始化绘制纹理的顶点缓存
 - (void)setupNormalVertexBuffer {
    float vertices[] = {
        -1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
        -1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
        1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
    };
    
    glGenBuffers(1, &_normalVertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, _normalVertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
  • 创建纹理
// 创建目标纹理
- (void)genTargetTexture {
    glGenTextures(1, &_textureID);
    glBindTexture(GL_TEXTURE_2D, _textureID);
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, [self drawableWidth], [self drawableHeight], 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _textureID, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

// 创建绘画纹理
- (void)genPaintTexture {
    glGenTextures(1, &_paintTextureID);
    glBindTexture(GL_TEXTURE_2D, _paintTextureID);
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, [self drawableWidth], [self drawableHeight], 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _paintTextureID, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

// 创建背景纹理
 - (void)genBackgroundTexture {
    if (self.backgroundImage) {
        self.backgroundTextureID = [YDWShaderHelper createTextureWithImage:self.backgroundImage];
    }
}
  • 初始化笔触纹理
- (void)setBrushTextureWithImageName:(NSString *)imageName
                          isFastMode:(BOOL)isFastMode {
    if (imageName.length == 0) {
        return;
    }
    if (isFastMode) {
        NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
        if (!textureIDs) {
            return;
        }
        self.brushTextureID = (GLuint)[[textureIDs firstObject] intValue];
    } else {
        // 加载纹理
        UIImage *image = [UIImage imageNamed:imageName];
        self.brushTextureID = [YDWShaderHelper createTextureWithImage:image];
        
        // 添加缓存
        NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
        if (!textureIDs) {
            textureIDs = [[NSMutableArray alloc] init];
        }
        [textureIDs addObject:@(self.brushTextureID)];
        [self.brushTextureCache setValue:textureIDs forKey:imageName];
    }
}

绘制曲线

OpenGL ES 中只有 点、直线、三角形这三种图元。因此,怎么在 OpenGL ES 中绘制曲线,是我们第一个要解决的问题,也是最复杂的问题。

一、绘制曲线连接的点

在 OpenGL ES 中绘制曲线的方式,就是将曲线拆分成点序列来绘制

因为要绘制点,所以采取的是点图元 。即要把顶点数据当成来绘制,并且每个点都要绘制出笔触的纹理。关键步骤如下:

  • 指定图元类型
	// 绘制前切换 Viewport
    glViewport(0, 0, [self drawableWidth], [self drawableHeight]);
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    // 绑定到绘画纹理
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.paintTextureID, 0);
    
    glUseProgram(self.brushProgram);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, self.brushTextureID);
    glUniform1i(glGetUniformLocation(self.brushProgram, "Texture"), 0);
    
    GLuint positionSlot = glGetAttribLocation(self.brushProgram, "Position");
    
    glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
    GLsizeiptr bufferSizeBytes = sizeof(Vertex) * self.vertexCount;
    glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_DYNAMIC_DRAW);
    
    glEnableVertexAttribArray(positionSlot);
    glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL + offsetof(Vertex, positionCoord));
    // 指定图元类型
    glDrawArrays(GL_POINTS, 0, self.vertexCount);
  • 顶点着色器
attribute vec4 Position;
uniform float Size;

void main (void) {
    gl_Position = Position;
    gl_PointSize = Size;
}
  • 片元着色器
    关键在于 gl_PointCoord 这个内置变量,当使用点图元的时候,可以通过这个变量获取到 当前像素在点图元中的归一化坐标。但是这个坐标的原点是在左上角,这和纹理坐标在竖直方向上是相反的。所以从纹理读取颜色的时候,要做一个 y 坐标的转换。
precision highp float;

uniform float R;
uniform float G;
uniform float B;
uniform float A;

uniform sampler2D Texture;

void main (void) {
    vec4 mask = texture2D(Texture, vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y));
    gl_FragColor = A * vec4(R, G, B, 1.0) * mask;
}

  • 通过 UITouch 来获取触摸点的位置,然后算出归一化的顶点坐标。
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    
    [self addPointWithTouches:touches];
}

- (void)addPointWithTouches:(NSSet<UITouch *> *)touches {
    UITouch *currentTouch = [touches anyObject];
    CGPoint previousPoint = [currentTouch previousLocationInView:self];
    CGPoint currentPoint = [currentTouch locationInView:self];
    
    // 起始点和当前的点重合,不需要绘制
    if (CGPointEqualToPoint(self.fromPoint, currentPoint)) {
        return;
    }
    
    CGPoint from = self.fromPoint;
    CGPoint to = middlePoint(previousPoint, currentPoint);
    CGPoint control = previousPoint;
    
    NSArray <NSValue *>*points = [YDWBezierCurvesTool pointsWithFrom:from
                                                                 to:to
                                                            control:control
                                                          pointSize:self.brushSize];
    if (points.count == 0) {
        return;
    }
    // 去除第一个点,避免与上次绘制的最后一个点重复
    NSMutableArray *mutPoints = [points mutableCopy];
    [mutPoints removeObjectAtIndex:0];
    points = [self verticesWithPoints:[mutPoints copy]];

    [self.pointsPreDraw addObjectsFromArray:points];
    [self drawPointsToScreen:points];
    
    self.fromPoint = to;
}

// UIKit 坐标点,转化为顶点坐标
- (NSArray <NSValue *>*)verticesWithPoints:(NSArray <NSValue *>*)points {
    NSMutableArray *mutArr = [[NSMutableArray alloc] init];
    for (int i = 0; i < points.count; ++i) {
        [mutArr addObject:@([self vertexWithPoint:points[i].CGPointValue])];
    }
    return [mutArr copy];
}

// 归一化顶点坐标
 - (CGPoint)vertexWithPoint:(CGPoint)point {
    float x = (point.x / self.frame.size.width) * 2 - 1;
    float y = 1 - (point.y / self.frame.size.height) * 2;
    return CGPointMake(x, y);
}
  • 由于 iOS 系统触摸事件的派发频率有限,最终得到的只能是稀疏的点。如下图所示,每个触摸点之间的间隔会比较大。

iOS之OpenGL ES实现手写“绘画板”_第1张图片

二、绘制密集的点

只需要在两个点之间,按照一定的密度进行插值,就可以绘制出连续的轨迹。但是如果绘制结果是折线,那么并不平滑。

iOS之OpenGL ES实现手写“绘画板”_第2张图片

三、使曲线变平滑
  • 解决点连接不平滑的问题,一般是使用贝塞尔曲线。具体的做法是使用两个顶点间的中点和 一个顶点 ,来构造一条贝塞尔曲线。如下图,图中的3 个红点被用来构造一条贝塞尔曲线。

iOS之OpenGL ES实现手写“绘画板”_第3张图片

  • 那么怎么在 OpenGL ES 中绘制贝塞尔曲线呢?相当于已知贝塞尔曲线的 3 个关键点,反向来求曲线上的点序列。
  • 贝塞尔曲线的方程是 P = (1 - t)^2 * P0 + 2 * t * (1 - t) * P1 + t^2 * P2, t 是唯一的变量,其取值范围是 0 ~ 1 。因此可以采取线性取值的方式,每一条贝塞尔曲线取 n 个点(n 是个确定的常量),只要依次往方程中代入 1 / n 、 2 / n 、 … n / n ,就可以得到一个点序列。

iOS之OpenGL ES实现手写“绘画板”_第4张图片

  • 先将 n 取一个比较小的值,这样比较容易看出存在的问题。我们发现,点序列的间隔并不均匀,原因有两个:
    • 不同贝塞尔曲线的长度不一样,使用同一个 n 值,算出来的点的疏密程度肯定不同。
    • 由于贝塞尔曲线随着 t 增长,曲线长度的增长并不是线性的。按照上面的算法,最终会得到的结果是 两头比较稀疏,中间比较密集 。
四、生成均匀的点序列
  • 贝塞尔曲线生成均匀的点序列,涉及到了一个经典的“贝塞尔曲线匀速运动”问题。
  • 在YDWBezierCurvesTool封装一个方法,只需要传入贝塞尔曲线的 3 个关键点和笔触尺寸,就可以获取均匀的点序列。
/**
 通过二次贝塞尔曲线的三个关键点,计算点序列

 @param from 起始点
 @param to 终止点
 @param control 控制点
 @param pointSize 画笔尺寸,用于计算生成点的数量
 @return 点序列
 */
 + (NSArray<NSValue *> *)pointsWithFrom:(CGPoint)from
                                    to:(CGPoint)to
                               control:(CGPoint)control
                             pointSize:(CGFloat)pointSize {

    CGPoint P0 = from;
    // 如果 control 是 from 和 to 的中点,则将 control 设置为和 from 重合
    CGPoint P1 = isCenter(control, from, to) ? from : control;
    CGPoint P2 = to;

    float ax = P0.x - 2 * P1.x + P2.x;
    float ay = P0.y - 2 * P1.y + P2.y;
    float bx = 2 * P1.x - 2 * P0.x;
    float by = 2 * P1.y - 2 * P0.y;
    
    float A = 4 * (ax * ax + ay * ay);
    float B = 4 * (ax * bx + ay * by);
    float C = bx * bx + by * by;
    
    // 整条曲线的长度
    float totalLength = [self lengthWithT:1 A:A B:B C:C];  
    // 用点的尺寸计算出,单位长度需要多少个点
    float pointsPerLength = 5.0 / pointSize;  
    // 曲线应该生成的点数
    int count = MAX(1, ceilf(pointsPerLength * totalLength));  
    
    NSMutableArray *mutArr = [[NSMutableArray alloc] init];
    for(int i = 0; i <= count; ++i) {
        float t = i * 1.0f / count;
        float length = t*totalLength;
        t = [self tWithT:t length:length A:A B:B C:C];
        // 根据 t 求出坐标
        float x = (1-t)*(1-t)*P0.x +2*(1-t)*t*P1.x + t*t*P2.x;
        float y = (1-t)*(1-t)*P0.y +2*(1-t)*t*P1.y + t*t*P2.y;
        [mutArr addObject:[NSValue valueWithCGPoint:CGPointMake(x, y)]];
    }
    return [mutArr copy];
}
  • 固定贝塞尔曲线的起始点和控制点,只移动终止点,来验证一下这个方法是否可靠。

iOS之OpenGL ES实现手写“绘画板”_第5张图片

  • 可以看到,在移动过程中,点和点的距离基本是保持一致的,并且是均匀的。通过这个方法,终于画出了平滑且均匀的曲线。

iOS之OpenGL ES实现手写“绘画板”_第6张图片

绘画板功能实现

一、颜色混合
  • 之前的 OpenGL ES 在开始一次渲染之前,都会调用 glClear(GL_COLOR_BUFFER_BIT) 来清除画布,因为不希望保留上次的渲染结果。
  • 对于一个绘画板来说,要不断地往画布上画东西,所以是希望保留上次结果的。因此,在绘制之前不能执行清除的操作。
  • 由于画笔可能是半透明的,所以新绘制的颜色需要和画布上已经存在的颜色进行混合,因此在绘制开始之前,需要开启混合选项。
// 绘制绘画的结果
- (void)renderPaint {
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glViewport(0, 0, [self drawableWidth], [self drawableHeight]);
    glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.textureID, 0);
    glBindBuffer(GL_ARRAY_BUFFER, self.normalVertexBuffer);
    
    glUseProgram(self.normalProgram);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, self.paintTextureID);
    glUniform1i(glGetUniformLocation(self.normalProgram, "Texture"), 0);
    
    GLuint positionSlot = glGetAttribLocation(self.normalProgram, "Position");
    GLuint textureSlot = glGetAttribLocation(self.normalProgram, "TextureCoords");
    
    glEnableVertexAttribArray(positionSlot);
    glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(textureSlot);
    glVertexAttribPointer(textureSlot, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3* sizeof(float)));
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
二、笔触调整

笔触有 3 个属性可以调整:颜色、尺寸、形状,它们本质上都是对点图元的调整,通过 uniform 变量的形式,将颜色、尺寸、纹理传入着色器并应用。

- (void)setBrushTextureUseFastModeIfCanWithImageName:(NSString *)imageName {
    NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
    [self setBrushTextureWithImageName:imageName isFastMode:textureIDs != nil];
}

- (void)setBrushTextureWithImageName:(NSString *)imageName
                          isFastMode:(BOOL)isFastMode {
    if (imageName.length == 0) {
        return;
    }
    if (isFastMode) {
        NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
        if (!textureIDs) {
            return;
        }
        self.brushTextureID = (GLuint)[[textureIDs firstObject] intValue];
    } else {
        // 加载纹理
        UIImage *image = [UIImage imageNamed:imageName];
        self.brushTextureID = [YDWShaderHelper createTextureWithImage:image];
        
        // 添加缓存
        NSMutableArray *textureIDs = [self.brushTextureCache valueForKey:imageName];
        if (!textureIDs) {
            textureIDs = [[NSMutableArray alloc] init];
        }
        [textureIDs addObject:@(self.brushTextureID)];
        [self.brushTextureCache setValue:textureIDs forKey:imageName];
    }
}
三、 橡皮擦

YDWPaintView在初始化的时候,需要传入一个背景色参数,当用户切换到橡皮擦功能的时候,内部只是单纯地将画笔的颜色切换成背景色,于是就产生了橡皮擦的效果。

- (void)setBrushMode:(GLPaintViewBrushMode)brushMode {
    _brushMode = brushMode;
    self.paintTexture.brushMode = (GLPaintTextureBrushMode)brushMode;
}
四、撤销重做

撤销重做功能需要依赖两个栈来实现,我们把用户的手指从按下屏幕到离开屏幕这一过程中产生的数据,定义为一个操作对象,这个操作对象保存了归一化后的点序列,以及点的属性。

/// 笔刷尺寸
@property (nonatomic, assign) CGFloat brushSize;
/// 笔刷颜色
@property (nonatomic, strong) UIColor *brushColor;
/// 笔刷模式
@property (nonatomic, assign) GLPaintViewBrushMode brushMode;
/// 笔触纹理图片文件名
@property (nonatomic, copy) NSString *brushImageName;
/// 点序列
@property (nonatomic, copy) NSArray<NSValue *> *points;

  • 撤销重做的实现逻辑
 - (void)undo {
    if ([self.operationStack isEmpty]) {
        return;
    }
    YDWPaintModel *model = self.operationStack.topModel;
    [self.operationStack popModel];
    [self.undoOperationStack pushModel:model];
    
    [self reDraw];
}

 - (void)redo {
    if ([self.undoOperationStack isEmpty]) {
        return;
    }
    YDWPaintModel *model = self.undoOperationStack.topModel;
    [self.undoOperationStack popModel];
    [self.operationStack pushModel:model];
    
    [self drawModel:model];
}
  • 由于撤销操作需要先清除画布,所以每次都需要重绘。而重做操作可以利用上次绘制的结果,所以每次只需要绘制一个步骤即可。

效果展示

iOS之OpenGL ES实现手写“绘画板”_第7张图片

完整示例

iOS之OpenGL ES实现手写“绘画板”

你可能感兴趣的:(iOS高级进阶,Swift高级进阶,OpenGL,ES,绘画板,贝塞尔曲线,自定义着色器,画笔笔触尺寸,橡皮擦)