MaLiang is a painting framework based on Metal. It supports drawing and handwriting with customized textures. The name of "MaLiang" comes from a boy who had a magical brush in Chinese ancient fairy story.
☕️ If I have saved your time, buy me a cup of coffee
📱 App based on MaLiang is now avaliable on the App Store
iOS 9.0, Swift 5
The core painting module is based on Metal
You can simply make it compatible with lower version of iOS and swift by changing only serval lines of code.
MaLiang is available through CocoaPods. To install
it, simply add the following line to your
To use the old OpenGL ES verion:
pod 'MaLiang', '~> 1.1'
To integrate MaLiang into your Xcode project using Carthage, specify it in your
carthage update to build the framework and drag the built
MaLiang.framework into your Xcode project.
Make sure to add
MaLiang.framework to your target's
MaLiang is simple to use.
open class Canvas: MetalView
Canvas is the basic component of
MaLiang. You will paint all things on it.
Canvas extends from
MetalView, whitch extends from
MetalView handles all the logic with MetalKit and hides them from you.
Canvas can be simply created with xib or code.
UIViewobject into your view controller and change it's class to
Canvasand module to
UIViewyou do before.
Now, all things necessary is done!
You can take snapshot on canvas now. Just call
snapshot function on
Canvas and you will get an optional
With all things done, you can do more with
Brush is the key feature to
MaLiang. It holds textures and colors, whitch makes it possiable to paint amazing things.
Brush with image data or file to Canvas and paint with it:
let path = Bundle.main.path(forResource: "pencil", ofType: "png")! let pencil = try? canvas.registerBrush(with: URL(fileURLWithPath: path)) pencil?.use()
Brush have serval properties for you to custmize:
// opacity of texture, affects the darkness of stroke // set opacity to 1 may cause heavy aliasing open var opacity: CGFloat = 0.3 // width of stroke line in points open var pointSize: CGFloat = 4 // this property defines the minimum distance (measureed in points) of nearest two textures // defaults to 1, this means erery texture calculated will be rendered, dictance calculation will be skiped open var pointStep: CGFloat = 1 // sensitive of pointsize changed from force, from 0 - 1 open var forceSensitive: CGFloat = 0 /// color of stroke open var color: UIColor = .black // indicate if the stroke size in visual will be scaled along with the Canvas // defaults to false, the stroke size in visual will stay with the original value open var scaleWithCanvas = false
With all these properties, you can create you own brush as your imagination.
MaLiang supports automatically adjustment of stroke size with painting force. 3D Touch is supported by default, and simulated force will be setup on devices those are not supporting this.
forceSensitive is the property that force affects the storke size. It should be set between
1. the smaller the value is, the less sensitive will be. if sets to
0, then force will not affects the stroke size.
Chartlet elements are supported from 2.1.0. A chartlet must be registered to canvas with its' texture data. You can simply get image data from its'
let data = UIImage(named: "chartlet").pngData() let texture = try canvas.makeTexture(with: data)
You can apply rotation to chartlet by passing a counter clockwise angle in radius when adding it to the canvas:
canvas.renderChartlet(at: location, size: chartletSize, textureID: texture.id, rotation: angle)
Text element can be rendered to canvas by the Chartlet feature. MaLiang leaves the work of text layout and styles to your self.
Refer to the samples for more details.
CanvasData is now configured by default. It holds all the data on the
Canvas, and makes the undo and redo actions to be possiable.
And you can implement your own saving logic with the data holds by
🎉 You can save your paintings to disk now.
// 1. create an instance of `DataExporter` with your canvas: let exporter = DataExporter(canvas: canvas) // 2. save to empty folders on disk: exporter.save(to: localPath, progress: progressHandler, result: resultHandler) // also you can use another synchronous method to do this work Synchronously exporter.saveSynchronously(to: locakPath, progress: progressHandler)
Then, contents of canvas and some document infomations will be saved to files in the directory you provided.
MaLiang does not zip the folders, you can implement your own archive Logics refer to the sample project
DataImporter to read data saved by
MaLiang to your canvas:
DataImporter.importData(from: localPath, to: canvas, progress: progressHandler, result: resultHandler)
Also, the localPath passed into DataImporter must be a folder where your contents files place. If you are using your own archive logic, unzip the contents first by your own.
MaLiang is available under the MIT license. See the LICENSE file for more info.