主頁 > 軟體設計 > iOS之OpenGL ES實作手寫“繪畫板”

iOS之OpenGL ES實作手寫“繪畫板”

2020-09-10 17:04:33 軟體設計

設定繪畫板

一、設定畫板視圖
  • 設定繪畫板位置、大小、背景
	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 系統觸摸事件的派發頻率有限,最終得到的只能是稀疏的點,如下圖所示,每個觸摸點之間的間隔會比較大,

在這里插入圖片描述

二、繪制密集的點

只需要在兩個點之間,按照一定的密度進行插值,就可以繪制出連續的軌跡,但是如果繪制結果是折線,那么并不平滑,

在這里插入圖片描述

三、使曲線變平滑
  • 解決點連接不平滑的問題,一般是使用貝塞爾曲線,具體的做法是使用兩個頂點間的中點和 一個頂點 ,來構造一條貝塞爾曲線,如下圖,圖中的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 ,就可以得到一個點序列,

在這里插入圖片描述

  • 先將 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];
}
  • 固定貝塞爾曲線的起始點和控制點,只移動終止點,來驗證一下這個方法是否可靠,

在這里插入圖片描述

  • 可以看到,在移動程序中,點和點的距離基本是保持一致的,并且是均勻的,通過這個方法,終于畫出了平滑且均勻的曲線,

在這里插入圖片描述

繪畫板功能實作

一、顏色混合
  • 之前的 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實作手寫“繪畫板”

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/2814.html

標籤:其他

上一篇:JZ51 構建乘積陣列

下一篇:2020-09-08

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more