This guide details implementation notes for Liquid. Read this guide to before contributing to Liquid. This guide may also be used as a learning tool in building other projects.
Readers should develop an understanding of:
Liquid is a Swift Package with three targets: Liquid, CLiquid, and LiquidTests. The Liquid target is distributed as the Liquid library.
Supported Platforms: iOS 13.0+, MacOS 10.15+
Libraries: SwiftUI, Combine, CoreGraphics, Accelerate
This target contains Swift source files for Liquid, organized in three sections: Processing, PrivateViews, PublicViews.
Dependency: CLiquid
The Processing folder contains helper-structs and extensions for computing operations. This is where most of the geometry logic for generating liquid blobs is calculated.
The PrivateViews folder contains underlying SwiftUI Shapes and Views that represent Liquid objects. For each type of Liquid object, there is a file containing a Shape struct and a file with the View struct. The Shape structs are responsible for calling processing functions to render a path. The View structs wrap the shapes and control the animatable data.
The PublicViews folder contains the public SwiftUI Views that the package users access. Liquid: View
, for example, wraps the private view types: LiquidPathView
and LiquidCircleView
.
This target contains C sources for Liquid. To understand why there is a C target, see Design Decisions: Using C. The Sources/CLiquid/include folder is used to host headers for the target.
This target contains Swift tests for Liquid. The tests aim to cover the accuracy of geometry calculations.
Liquid implements methods for generating liquid circles and liquid paths from custom source paths.
Bezier control points define the shape of an arc. For a consistent path, the last control point of an arc should be tangent to the following point and the first control point of the next arc.
To maintain a continuos path, Liquid ensures adjacent control points are always tangent. The tangent is found from the slope of neighboring points. The magnitude of the control points along the tangent line is 1/3 of the magnitude of the distance between the points defining the arc.
Generate n
random values in the range of 0 to 2pi. To ensure the path maintains definition, the circle is divided into n
slices with one point lying in each. These values are radians that will define anchor points for path generation. Using the radian values, find cartesian coordinates at the frame's radius.
For each point in the anchor array, use the slope between the previous and next neighbors to define the local tangent. For an arc between two points, the first control point is found by:
- Calculating the distance between the two points.
- travelling 1/3 of this distance in the direction of the left point's local tangent.
The second control point is found by:
- Calculating the distance between the two points.
- Travelling -1/3 of this distance in the direction of the right point's local tangent.
Turn any path into a set of points. The extracted points are usually the end points of lines and curves. Linearly interpolate some number of points. This becomes a point cloud to sample.
Select n
points from the point cloud. To ensure the path maintains definition, the cloud is divided into n
segments with one point selected from each.
For each point in the anchor array, use the slope between the previous and next neighbors to define the local tangent. For an arc between two points, the first control point is found by: 1) calculating the distance between the two points. 2) travelling 1/3 of this distance in the direction of the left point's local tangent. The first control point is found by: 1) calculating the distance between the two points. 2) travelling -1/3 of this distance in the direction of the right point's local tangent.
For the batch geometric operation used to find Bezier controls, a C method is used. There is an argument for using C in large numeric operations because it is faster than Swift. Because the geometric operations happen at every animation frame, Liquid uses C for a performance bump.
In a trial with one million points, a Swift-equivalent method took 0.78 seconds, while the C method performed in 0.035 seconds. The trial shows over 20x faster speeds with C. To conduct this trial on a local machine, see Tests/LiquidTests/PerformanceTests.swift
.
While a circle could be sampled according to the same processing as a regular path, there is a benefit in constructing the circle as its own path. For a circle, the radians are animated; allowing the anchor points to always stay on the source path. For a custom path, the x and y coordinates are linearly animated, which makes the anchor points deviate from the source path during animation