Skip to content

Commit

Permalink
optimize stroke with bezier path
Browse files Browse the repository at this point in the history
  • Loading branch information
Harley-xk committed Nov 10, 2017
1 parent ffc281b commit 2c6ae4d
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 24 deletions.
89 changes: 89 additions & 0 deletions BezierGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// BezierGenerator.swift
// MaLiang
//
// Created by Harley.xk on 2017/11/10.
//

import UIKit

class BezierGenerator {

enum Style {
case linear
case quadratic // this is the only style currently supported
case cubic
}

// var segements = 20

init() {
}

init(beginPoint: CGPoint) {
begin(with: beginPoint)
}

func begin(with point: CGPoint) {
points.append(point)
}

func pushPoint(_ point: CGPoint) -> [CGPoint] {
points.append(point)
if points.count < style.pointCount {
return []
}
step += 1
let result = genericPathPoints()
return result
}

func finish() {
step = 0
points.removeAll()
}

private var points: [CGPoint] = []
private var style: Style = .quadratic

private var step = 0
private func genericPathPoints() -> [CGPoint] {

var begin: CGPoint
var control: CGPoint
let end = CGPoint.middle(p1: points[step], p2: points[step + 1])

var vertices: [CGPoint] = []
if step == 1 {
begin = points[0]
let middle1 = CGPoint.middle(p1: points[0], p2: points[1])
control = CGPoint.middle(p1: middle1, p2: points[1])
} else {
begin = CGPoint.middle(p1: points[step - 1], p2: points[step])
control = points[step]
}

/// segements are based on distance about start and end point
let dis = begin.distance(to: end)
let segements = max(Int(dis / 5), 2)

for i in 0 ..< segements {
let t = CGFloat(i) / CGFloat(segements)
let x = pow(1 - t, 2) * begin.x + 2.0 * (1 - t) * t * control.x + t * t * end.x
let y = pow(1 - t, 2) * begin.y + 2.0 * (1 - t) * t * control.y + t * t * end.y
vertices.append(CGPoint(x: x, y: y))
}
vertices.append(end)
return vertices
}
}

extension BezierGenerator.Style {
var pointCount: Int {
switch self {
case .quadratic: return 3
default: return Int.max
}
}
}

2 changes: 1 addition & 1 deletion Example/MaLiang/Base.lproj/LaunchScreen.xib
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2015 CocoaPods. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2015 Harley-xk. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
Expand Down
10 changes: 10 additions & 0 deletions Example/MaLiang/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
Expand All @@ -32,10 +33,19 @@
<action selector="styleChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="flD-Jd-1qp"/>
</connections>
</segmentedControl>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="X7T-Is-9js">
<rect key="frame" x="318" y="581" width="36" height="30"/>
<state key="normal" title="Clear"/>
<connections>
<action selector="clearAction:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="YoX-yV-8pC"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="AID-gC-Kue" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="Aqn-Ev-Yxs"/>
<constraint firstItem="AID-gC-Kue" firstAttribute="top" secondItem="X7T-Is-9js" secondAttribute="bottom" constant="8" id="KcT-3X-OMO"/>
<constraint firstAttribute="trailingMargin" secondItem="X7T-Is-9js" secondAttribute="trailing" constant="5" id="OvN-q6-WAK"/>
<constraint firstAttribute="trailing" secondItem="AID-gC-Kue" secondAttribute="trailing" constant="16" id="dSQ-CA-N3L"/>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="AID-gC-Kue" secondAttribute="bottom" constant="20" id="yRe-lP-8Pf"/>
</constraints>
Expand Down
Binary file modified Example/MaLiang/Images.xcassets/brush.imageset/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Example/MaLiang/Images.xcassets/pen.imageset/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions Example/MaLiang/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ class ViewController: UIViewController {
// Do any additional setup after loading the view, typically from a nib.

let pen = Brush(texture: #imageLiteral(resourceName: "pen"))
pen.scale = 20
pen.opacity = 0.8
pen.strokeWidth = 30
pen.opacity = 0.1
canvas.brush = pen

let pencil = Brush(texture: #imageLiteral(resourceName: "pencil"))
pencil.scale = 10
pencil.opacity = 0.1
pencil.strokeWidth = 3
pencil.opacity = 0.4

let brush = Brush(texture: #imageLiteral(resourceName: "brush"))
brush.scale = 1
brush.strokeWidth = 30

brushes = [pen, pencil, brush]
}
Expand All @@ -47,5 +47,9 @@ class ViewController: UIViewController {
let brush = brushes[index]
canvas.brush = brush
}

@IBAction func clearAction(_ sender: Any) {
canvas.erase()
}
}

4 changes: 4 additions & 0 deletions Example/Pods/Pods.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 17 additions & 8 deletions MaLiang/Classes/Brush.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,24 @@ import UIKit

open class Brush {

open var opacity: Float = 0.3

open var strokeWidth: Int = 4

// open var width: CGFloat = 1 {
// didSet {
// scale = scale / width
// }
// }

var id: GLuint = 0
var width: GLsizei = 0
var height: GLsizei = 0
var textureWidth: GLsizei = 0
var textureHeight: GLsizei = 0

var texture: UIImage
open var opacity: Float = 0.3
open var pixelStep = 3
open var scale = 2

var scale: CGFloat = 10
var pixelStep = 3

public init(texture: UIImage) {
self.texture = texture
}
Expand Down Expand Up @@ -57,8 +66,8 @@ open class Brush {
// Release the image data; it's no longer needed

self.id = texId
self.width = width.int32
self.height = height.int32
self.textureWidth = width.int32
self.textureHeight = height.int32
}
}
}
48 changes: 38 additions & 10 deletions MaLiang/Classes/Canvas.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ open class Canvas: UIView {
if initialized {
brush.createTexture()
glUseProgram(programs[ShaderProgram.point].id)
glUniform1f(programs[ShaderProgram.point].uniform[Uniform.pointSize], brush.width.float / brush.scale.float)
glUniform1f(programs[ShaderProgram.point].uniform[Uniform.pointSize], GLfloat(brush.strokeWidth) * GLfloat(contentScaleFactor))

// alpha changed with different brushes, so color needs to be reset
resetColor()
}
Expand All @@ -55,6 +55,9 @@ open class Canvas: UIView {
}
}

// optimize stroke with bezier path, defaults to true
private var enableBezierPath = true

// MARK: - Functions
// Erases the screen
open func erase() {
Expand Down Expand Up @@ -110,6 +113,8 @@ open class Canvas: UIView {
private var location: CGPoint = CGPoint()
private var previousLocation: CGPoint = CGPoint()

private var bezierGenerator = BezierGenerator()

// Implement this to override the default layer class (which is [CALayer class]).
// We do this so that our view will be backed by a layer that is capable of OpenGL ES rendering.
override open class var layerClass : AnyClass {
Expand Down Expand Up @@ -244,8 +249,8 @@ open class Canvas: UIView {
}

// point size
glUniform1f(programs[ShaderProgram.point].uniform[Uniform.pointSize], brush.width.float / brush.scale.float)
glUniform1f(programs[ShaderProgram.point].uniform[Uniform.pointSize], GLfloat(brush.strokeWidth) * GLfloat(contentScaleFactor))

// initialize brush color
glUniform4fv(programs[ShaderProgram.point].uniform[Uniform.vertexColor], 1, brushColor.glcolor)

Expand Down Expand Up @@ -396,7 +401,7 @@ open class Canvas: UIView {

// Allocate vertex array buffer
var vertexBuffer: [GLfloat] = []

// Add points to the buffer so there are drawing points every X pixels
let count = max(Int(ceilf(sqrtf((end.x - start.x).float * (end.x - start.x).float + (end.y - start.y).float * (end.y - start.y).float) / brush.pixelStep.float)), 1)
vertexBuffer.reserveCapacity(count * 2)
Expand All @@ -419,12 +424,16 @@ open class Canvas: UIView {
glDrawArrays(GL_POINTS.gluint, 0, count.int32)

if display {
// Display the buffer
glBindRenderbuffer(GL_RENDERBUFFER.gluint, viewRenderbuffer)
context.presentRenderbuffer(GL_RENDERBUFFER.int)
displayBuffer()
}
}

private func displayBuffer() {
// Display the buffer
glBindRenderbuffer(GL_RENDERBUFFER.gluint, viewRenderbuffer)
context.presentRenderbuffer(GL_RENDERBUFFER.int)
}

// MARK: - Gestures

// Handles the start of a touch
Expand All @@ -435,6 +444,10 @@ open class Canvas: UIView {
// Convert touch point from UIView referential to OpenGL one (upside-down flip)
location = touch.location(in: self)
location.y = bounds.size.height - location.y

if enableBezierPath {
bezierGenerator.begin(with: location)
}
}

// Handles the continuation of a touch.
Expand All @@ -454,8 +467,19 @@ open class Canvas: UIView {
location = touch.location(in: self)
location.y = bounds.size.height - location.y

// Render the stroke
self.renderLine(from: previousLocation, to: location)
if enableBezierPath {
let vertices = bezierGenerator.pushPoint(location)
if vertices.count >= 2 {
for i in 0 ..< vertices.count - 1 {
self.renderLine(from: vertices[i], to: vertices[i + 1], display: false)
}
}
displayBuffer()
} else {
// Render the stroke
self.renderLine(from: previousLocation, to: location)
}

}

// Handles the end of a touch event when the touch is a tap.
Expand All @@ -468,6 +492,10 @@ open class Canvas: UIView {
previousLocation.y = bounds.size.height - previousLocation.y
self.renderLine(from: previousLocation, to: location)
}

if enableBezierPath {
bezierGenerator.finish()
}
}

// Handles the end of a touch event.
Expand Down
14 changes: 14 additions & 0 deletions MaLiang/Classes/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,17 @@ extension UIColor {
}
}

// MARK: - Point Utils
extension CGPoint {
static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5)
}

func distance(to other: CGPoint) -> CGFloat {
let p = pow(x - other.x, 2) + pow(y - other.y, 2)
return sqrt(p)
}
}



0 comments on commit 2c6ae4d

Please sign in to comment.