mirror of
https://github.com/exyte/Macaw.git
synced 2024-09-21 01:47:44 +03:00
Better Align implementation
This commit is contained in:
parent
26ee61a2c8
commit
49e16e9f52
@ -1,5 +1,44 @@
|
||||
public enum Align {
|
||||
case min
|
||||
case mid
|
||||
case max
|
||||
open class Align {
|
||||
|
||||
public static let min: Align = MinAlign()
|
||||
public static let mid: Align = MidAlign()
|
||||
public static let max: Align = MaxAlign()
|
||||
|
||||
open func align(x: Double, y: Double) -> Double {
|
||||
return 0
|
||||
}
|
||||
|
||||
open func align(x: CGFloat, y: CGFloat) -> CGFloat {
|
||||
return CGFloat(align(x: x.doubleValue, y: y.doubleValue))
|
||||
}
|
||||
|
||||
open func align(x: Double) -> Double {
|
||||
return align(x: x, y: 0)
|
||||
}
|
||||
|
||||
open func align(x: CGFloat) -> CGFloat {
|
||||
return CGFloat(align(x: x.doubleValue))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MinAlign : Align {
|
||||
|
||||
override func align(x: Double, y: Double) -> Double {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private class MidAlign : Align {
|
||||
|
||||
override func align(x: Double, y: Double) -> Double {
|
||||
return x / 2 - y / 2
|
||||
}
|
||||
}
|
||||
|
||||
private class MaxAlign : Align {
|
||||
|
||||
override func align(x: Double, y: Double) -> Double {
|
||||
return x - y
|
||||
}
|
||||
}
|
||||
|
@ -121,16 +121,7 @@ open class Text: Node {
|
||||
NSAttributedStringKey.font: font
|
||||
]
|
||||
let textSize = NSString(string: text).size(withAttributes: textAttributes)
|
||||
var alignmentOffset = 0.0
|
||||
switch align {
|
||||
case .mid:
|
||||
alignmentOffset = (textSize.width / 2).doubleValue
|
||||
case .max:
|
||||
alignmentOffset = textSize.width.doubleValue
|
||||
default:
|
||||
break
|
||||
}
|
||||
return -alignmentOffset
|
||||
return -align.align(x: textSize.width.doubleValue)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -115,8 +115,6 @@ class ImageRenderer: NodeRenderer {
|
||||
let srcAR = size.width / size.height
|
||||
var resultW = w
|
||||
var resultH = h
|
||||
var destX = CGFloat(0)
|
||||
var destY = CGFloat(0)
|
||||
if destAR < srcAR {
|
||||
// fill all available width and scale height
|
||||
resultH = size.height * w / size.width
|
||||
@ -124,24 +122,8 @@ class ImageRenderer: NodeRenderer {
|
||||
// fill all available height and scale width
|
||||
resultW = size.width * h / size.height
|
||||
}
|
||||
let xalign = image.xAlign
|
||||
switch xalign {
|
||||
case Align.min:
|
||||
destX = 0
|
||||
case Align.mid:
|
||||
destX = w / 2 - resultW / 2
|
||||
case Align.max:
|
||||
destX = w - resultW
|
||||
}
|
||||
let yalign = image.yAlign
|
||||
switch yalign {
|
||||
case Align.min:
|
||||
destY = 0
|
||||
case Align.mid:
|
||||
destY = h / 2 - resultH / 2
|
||||
case Align.max:
|
||||
destY = h - resultH
|
||||
}
|
||||
let destX = image.xAlign.align(x: w, y: resultW)
|
||||
let destY = image.yAlign.align(x: h, y: resultH)
|
||||
return CGRect(x: destX, y: destY, width: resultW, height: resultH)
|
||||
}
|
||||
|
||||
@ -159,26 +141,12 @@ class ImageRenderer: NodeRenderer {
|
||||
// fill all available width and scale height
|
||||
totalH = size.height * w / size.width
|
||||
totalW = w
|
||||
switch image.yAlign {
|
||||
case Align.min:
|
||||
srcY = 0
|
||||
case Align.mid:
|
||||
srcY = -(totalH / 2 - h / 2)
|
||||
case Align.max:
|
||||
srcY = -(totalH - h)
|
||||
}
|
||||
srcY = image.yAlign.align(x: h, y: totalH)
|
||||
} else {
|
||||
// fill all available height and scale width
|
||||
totalW = size.width * h / size.height
|
||||
totalH = h
|
||||
switch image.xAlign {
|
||||
case Align.min:
|
||||
srcX = 0
|
||||
case Align.mid:
|
||||
srcX = -(totalW / 2 - w / 2)
|
||||
case Align.max:
|
||||
srcX = -(totalW - w)
|
||||
}
|
||||
srcX = image.xAlign.align(x: w, y: totalW)
|
||||
}
|
||||
return CGRect(x: srcX, y: srcY, width: totalW, height: totalH)
|
||||
}
|
||||
|
@ -145,16 +145,7 @@ class TextRenderer: NodeRenderer {
|
||||
NSAttributedStringKey.font: font
|
||||
]
|
||||
let textSize = NSString(string: text.text).size(withAttributes: textAttributes)
|
||||
var alignmentOffset = CGFloat(0)
|
||||
switch text.align {
|
||||
case Align.mid:
|
||||
alignmentOffset = textSize.width / 2
|
||||
case Align.max:
|
||||
alignmentOffset = textSize.width
|
||||
default:
|
||||
break
|
||||
}
|
||||
return -alignmentOffset
|
||||
return -text.align.align(x: textSize.width)
|
||||
}
|
||||
|
||||
fileprivate func getTextColor(_ fill: Fill) -> MColor {
|
||||
|
@ -10,27 +10,35 @@ import CoreGraphics
|
||||
///
|
||||
|
||||
open class SVGParser {
|
||||
|
||||
fileprivate class ViewBoxParams {
|
||||
var svgSize: Size?
|
||||
var viewBox: Rect?
|
||||
var scalingMode: AspectRatio?
|
||||
var xAligningMode: Align?
|
||||
var yAligningMode: Align?
|
||||
}
|
||||
|
||||
/// Parse an SVG file identified by the specified bundle, name and file extension.
|
||||
/// - returns: Root node of the corresponding Macaw scene.
|
||||
open class func parse(bundle: Bundle, path: String, ofType: String = "svg", transformHelper: TransformHelperProtocol = TransformHelper()) throws -> Node {
|
||||
open class func parse(bundle: Bundle, path: String, ofType: String = "svg") throws -> Node {
|
||||
guard let fullPath = bundle.path(forResource: path, ofType: ofType) else {
|
||||
throw SVGParserError.noSuchFile(path: "\(path).\(ofType)")
|
||||
}
|
||||
let text = try String(contentsOfFile: fullPath, encoding: String.Encoding.utf8)
|
||||
return try SVGParser.parse(text: text, transformHelper: transformHelper)
|
||||
return try SVGParser.parse(text: text)
|
||||
}
|
||||
|
||||
/// Parse an SVG file identified by the specified name and file extension.
|
||||
/// - returns: Root node of the corresponding Macaw scene.
|
||||
open class func parse(path: String, ofType: String = "svg", transformHelper: TransformHelperProtocol = TransformHelper()) throws -> Node {
|
||||
return try SVGParser.parse(bundle: Bundle.main, path: path, ofType: ofType, transformHelper: transformHelper)
|
||||
open class func parse(path: String, ofType: String = "svg") throws -> Node {
|
||||
return try SVGParser.parse(bundle: Bundle.main, path: path, ofType: ofType)
|
||||
}
|
||||
|
||||
/// Parse the specified content of an SVG file.
|
||||
/// - returns: Root node of the corresponding Macaw scene.
|
||||
open class func parse(text: String, transformHelper: TransformHelperProtocol = TransformHelper()) throws -> Node {
|
||||
return SVGParser(text, transformHelper: transformHelper).parse()
|
||||
open class func parse(text: String) throws -> Node {
|
||||
return SVGParser(text).parse()
|
||||
}
|
||||
|
||||
let availableStyleAttributes = ["stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-linecap", "stroke-linejoin",
|
||||
@ -41,13 +49,6 @@ open class SVGParser {
|
||||
|
||||
fileprivate let xmlString: String
|
||||
fileprivate let initialPosition: Transform
|
||||
|
||||
fileprivate var svgSize: Size?
|
||||
fileprivate var viewBox: Rect?
|
||||
fileprivate var scalingMode: ScaleMode?
|
||||
fileprivate var xAligningMode: AlignMode?
|
||||
fileprivate var yAligningMode: AlignMode?
|
||||
fileprivate var transformHelper: TransformHelperProtocol!
|
||||
|
||||
fileprivate var nodes = [Node]()
|
||||
fileprivate var defNodes = [String: XMLIndexer]()
|
||||
@ -68,32 +69,36 @@ open class SVGParser {
|
||||
|
||||
fileprivate typealias PathCommand = (type: PathCommandType, expression: String, absolute: Bool)
|
||||
|
||||
fileprivate init(_ string: String, pos: Transform = Transform(), transformHelper: TransformHelperProtocol = TransformHelper()) {
|
||||
fileprivate init(_ string: String, pos: Transform = Transform()) {
|
||||
self.xmlString = string
|
||||
self.initialPosition = pos
|
||||
self.transformHelper = transformHelper
|
||||
}
|
||||
|
||||
fileprivate func parse() -> Group {
|
||||
let parsedXml = SWXMLHash.parse(xmlString)
|
||||
prepareSvg(parsedXml.children)
|
||||
|
||||
var viewBoxParams: ViewBoxParams?
|
||||
for child in parsedXml.children {
|
||||
if let element = child.element {
|
||||
if element.name == "svg" {
|
||||
viewBoxParams = parseViewBox(element)
|
||||
prepareSvg(child.children)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
parseSvg(parsedXml.children)
|
||||
|
||||
let group = Group(contents: self.nodes, place: initialPosition)
|
||||
addViewBoxClip(toNode: group)
|
||||
if let viewBoxParams = viewBoxParams {
|
||||
addViewBoxClip(toNode: group, viewBoxParams: viewBoxParams)
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
||||
|
||||
fileprivate func prepareSvg(_ children: [XMLIndexer]) {
|
||||
children.forEach { child in
|
||||
if let element = child.element {
|
||||
if element.name == "svg" {
|
||||
parseViewBox(element)
|
||||
prepareSvg(child.children)
|
||||
} else {
|
||||
prepareSvg(child)
|
||||
}
|
||||
}
|
||||
prepareSvg(child)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,57 +128,59 @@ open class SVGParser {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func addViewBoxClip(toNode node: Node) {
|
||||
fileprivate func addViewBoxClip(toNode node: Node, viewBoxParams params: ViewBoxParams) {
|
||||
|
||||
guard let viewBox = viewBox else { return }
|
||||
guard let viewBox = params.viewBox else { return }
|
||||
node.clip = viewBox
|
||||
|
||||
guard let scalingMode = scalingMode else { return }
|
||||
guard let svgSize = svgSize else { return }
|
||||
guard let scalingMode = params.scalingMode else { return }
|
||||
guard let svgSize = params.svgSize else { return }
|
||||
|
||||
if scalingMode == .aspectFill {
|
||||
if scalingMode == .slice {
|
||||
// setup new clipping to slice extra bits
|
||||
node.clip = svgSize.aspectFit(viewBox)
|
||||
}
|
||||
|
||||
transformHelper.scalingMode = scalingMode
|
||||
transformHelper.xAligningMode = xAligningMode
|
||||
transformHelper.yAligningMode = yAligningMode
|
||||
let transformHelper = TransformHelper(scalingMode: scalingMode, xAligningMode: params.xAligningMode, yAligningMode: params.yAligningMode)
|
||||
node.place = transformHelper.getTransformOf(viewBox, into: Rect(x: 0, y: 0, w: svgSize.w, h: svgSize.h))
|
||||
|
||||
// move to (0, 0)
|
||||
node.place = node.place.move(dx: -viewBox.x, dy: -viewBox.y)
|
||||
}
|
||||
|
||||
fileprivate func parseViewBox(_ element: SWXMLHash.XMLElement) {
|
||||
fileprivate func parseViewBox(_ element: SWXMLHash.XMLElement) -> ViewBoxParams {
|
||||
let params = ViewBoxParams()
|
||||
|
||||
if let w = getDoubleValue(element, attribute: "width"), let h = getDoubleValue(element, attribute: "height") {
|
||||
svgSize = Size(w: w, h: h)
|
||||
params.svgSize = Size(w: w, h: h)
|
||||
}
|
||||
if let viewBoxString = element.allAttributes["viewBox"]?.text {
|
||||
let nums = viewBoxString.components(separatedBy: .whitespaces).map{ Double($0) }
|
||||
if nums.count == 4, let x = nums[0], let y = nums[1], let w = nums[2], let h = nums[3] {
|
||||
viewBox = Rect(x: x, y: y, w: w, h: h)
|
||||
params.viewBox = Rect(x: x, y: y, w: w, h: h)
|
||||
}
|
||||
}
|
||||
if let contentModeString = element.allAttributes["preserveAspectRatio"]?.text {
|
||||
let strings = contentModeString.components(separatedBy: CharacterSet(charactersIn: " "))
|
||||
if strings.count == 1 { // none
|
||||
scalingMode = ScaleMode(rawValue: strings[0])
|
||||
return
|
||||
params.scalingMode = parseAspectRatio(strings[0])
|
||||
return params
|
||||
}
|
||||
guard strings.count == 2 else { return }
|
||||
guard strings.count == 2 else { return params }
|
||||
|
||||
let alignString = strings[0]
|
||||
var xAlign = alignString.prefix(4).lowercased()
|
||||
xAlign.remove(at: xAlign.startIndex)
|
||||
xAligningMode = AlignMode(rawValue: xAlign)
|
||||
params.xAligningMode = parseAlign(xAlign)
|
||||
|
||||
var yAlign = alignString.suffix(4).lowercased()
|
||||
yAlign.remove(at: yAlign.startIndex)
|
||||
yAligningMode = AlignMode(rawValue: yAlign)
|
||||
params.yAligningMode = parseAlign(yAlign)
|
||||
|
||||
scalingMode = ScaleMode(rawValue: strings[1])
|
||||
params.scalingMode = parseAspectRatio(strings[1])
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
fileprivate func parseNode(_ node: XMLIndexer, groupStyle: [String: String] = [:]) -> Node? {
|
||||
@ -340,6 +347,26 @@ open class SVGParser {
|
||||
}
|
||||
return parseTransformationAttribute(transformAttribute)
|
||||
}
|
||||
|
||||
fileprivate func parseAlign(_ string: String) -> Align {
|
||||
if string == "min" {
|
||||
return .min
|
||||
}
|
||||
if string == "mid" {
|
||||
return .mid
|
||||
}
|
||||
return .max
|
||||
}
|
||||
|
||||
fileprivate func parseAspectRatio(_ string: String) -> AspectRatio {
|
||||
if string == "meet" {
|
||||
return .meet
|
||||
}
|
||||
if string == "slice" {
|
||||
return .slice
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
var count = 0
|
||||
|
||||
|
@ -157,16 +157,15 @@ open class SVGSerializer {
|
||||
result += SVGGenericCloseTag
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
fileprivate func alignToSVG(_ align: Align) -> String {
|
||||
switch align {
|
||||
case .mid:
|
||||
if align === Align.mid {
|
||||
return " text-anchor=\"middle\" "
|
||||
case .max:
|
||||
return " text-anchor=\"end "
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
if align === Align.max {
|
||||
return " text-anchor=\"end "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fileprivate func baselineToSVG(_ baseline: Baseline) -> String {
|
||||
|
@ -56,44 +56,44 @@ open class SVGView: MacawView {
|
||||
let svgWidth = nodeBounds.width
|
||||
let svgHeight = nodeBounds.height
|
||||
|
||||
let transformHelper = TransformHelper()
|
||||
transformHelper.scalingMode = .noScaling
|
||||
transformHelper.xAligningMode = .mid
|
||||
transformHelper.yAligningMode = .mid
|
||||
var scalingMode = AspectRatio.none
|
||||
var xAligningMode = Align.mid
|
||||
var yAligningMode = Align.mid
|
||||
|
||||
switch self.contentMode {
|
||||
switch contentMode {
|
||||
case .scaleToFill:
|
||||
transformHelper.scalingMode = .scaleToFill
|
||||
scalingMode = .none
|
||||
case .scaleAspectFill:
|
||||
transformHelper.scalingMode = .aspectFill
|
||||
scalingMode = .slice
|
||||
case .scaleAspectFit:
|
||||
transformHelper.scalingMode = .aspectFit
|
||||
scalingMode = .meet
|
||||
case .center:
|
||||
break
|
||||
case .top:
|
||||
transformHelper.yAligningMode = .min
|
||||
yAligningMode = .min
|
||||
case .bottom:
|
||||
transformHelper.yAligningMode = .max
|
||||
yAligningMode = .max
|
||||
case .left:
|
||||
transformHelper.xAligningMode = .min
|
||||
xAligningMode = .min
|
||||
case .right:
|
||||
transformHelper.xAligningMode = .max
|
||||
xAligningMode = .max
|
||||
case .topLeft:
|
||||
transformHelper.xAligningMode = .min
|
||||
transformHelper.yAligningMode = .min
|
||||
xAligningMode = .min
|
||||
yAligningMode = .min
|
||||
case .topRight:
|
||||
transformHelper.xAligningMode = .max
|
||||
transformHelper.yAligningMode = .min
|
||||
xAligningMode = .max
|
||||
yAligningMode = .min
|
||||
case .bottomLeft:
|
||||
transformHelper.xAligningMode = .min
|
||||
transformHelper.yAligningMode = .max
|
||||
xAligningMode = .min
|
||||
yAligningMode = .max
|
||||
case .bottomRight:
|
||||
transformHelper.xAligningMode = .max
|
||||
transformHelper.yAligningMode = .max
|
||||
xAligningMode = .max
|
||||
yAligningMode = .max
|
||||
case .redraw:
|
||||
break
|
||||
}
|
||||
|
||||
let transformHelper = TransformHelper(scalingMode: scalingMode, xAligningMode: xAligningMode, yAligningMode: yAligningMode)
|
||||
svgNode.place = transformHelper.getTransformOf(Rect(x: 0, y: 0, w: Double(svgWidth), h: Double(svgHeight)), into: Rect(cgRect: viewBounds))
|
||||
|
||||
rootNode.contents = [svgNode]
|
||||
|
@ -1,33 +1,25 @@
|
||||
|
||||
public enum AlignMode : String {
|
||||
case max
|
||||
case mid
|
||||
case min
|
||||
}
|
||||
|
||||
public enum ScaleMode : String {
|
||||
case aspectFit = "meet"
|
||||
case aspectFill = "slice"
|
||||
case scaleToFill = "none"
|
||||
case noScaling = "noScaling"
|
||||
}
|
||||
|
||||
public protocol TransformHelperProtocol {
|
||||
|
||||
var scalingMode: ScaleMode? { get set }
|
||||
var xAligningMode: AlignMode? { get set }
|
||||
var yAligningMode: AlignMode? { get set }
|
||||
|
||||
static var standard: TransformHelperProtocol { get }
|
||||
func getTransformOf(_ rect: Rect, into rectToFitIn: Rect) -> Transform
|
||||
}
|
||||
|
||||
open class TransformHelper: TransformHelperProtocol {
|
||||
|
||||
public var scalingMode: ScaleMode?
|
||||
public var xAligningMode: AlignMode?
|
||||
public var yAligningMode: AlignMode?
|
||||
public let scalingMode: AspectRatio!
|
||||
public let xAligningMode: Align!
|
||||
public let yAligningMode: Align!
|
||||
|
||||
public init() { }
|
||||
public init(scalingMode: AspectRatio, xAligningMode: Align? = Align.min, yAligningMode: Align? = Align.min) {
|
||||
self.scalingMode = scalingMode
|
||||
self.xAligningMode = xAligningMode
|
||||
self.yAligningMode = yAligningMode
|
||||
}
|
||||
|
||||
public static var standard: TransformHelperProtocol {
|
||||
return TransformHelper(scalingMode: .none)
|
||||
}
|
||||
|
||||
public func getTransformOf(_ rect: Rect, into rectToFitIn: Rect) -> Transform {
|
||||
|
||||
@ -41,7 +33,7 @@ open class TransformHelper: TransformHelperProtocol {
|
||||
var newHeight = rectToFitIn.h
|
||||
|
||||
switch scalingMode {
|
||||
case .aspectFit:
|
||||
case .meet:
|
||||
if heightRatio < widthRatio {
|
||||
newWidth = rect.w * heightRatio
|
||||
} else {
|
||||
@ -51,7 +43,7 @@ open class TransformHelper: TransformHelperProtocol {
|
||||
sx: newWidth / rect.w,
|
||||
sy: newHeight / rect.h
|
||||
)
|
||||
case .aspectFill:
|
||||
case .slice:
|
||||
if heightRatio > widthRatio {
|
||||
newWidth = rect.w * heightRatio
|
||||
} else {
|
||||
@ -61,47 +53,16 @@ open class TransformHelper: TransformHelperProtocol {
|
||||
sx: newWidth / rect.w,
|
||||
sy: newHeight / rect.h
|
||||
)
|
||||
case .scaleToFill:
|
||||
case .none:
|
||||
result = result.scale(
|
||||
sx: Double(widthRatio),
|
||||
sy: Double(heightRatio)
|
||||
)
|
||||
case .noScaling:
|
||||
newWidth = rect.w
|
||||
newHeight = rect.h
|
||||
}
|
||||
|
||||
guard let xAligningMode = xAligningMode else { return result }
|
||||
switch xAligningMode {
|
||||
case .min:
|
||||
break
|
||||
case .mid:
|
||||
result = result.move(
|
||||
dx: (rectToFitIn.w / 2 - newWidth / 2) / (newWidth / rect.w),
|
||||
dy: 0
|
||||
)
|
||||
case .max:
|
||||
result = result.move(
|
||||
dx: (rectToFitIn.w - newWidth) / (newWidth / rect.w),
|
||||
dy: 0
|
||||
)
|
||||
}
|
||||
|
||||
guard let yAligningMode = yAligningMode else { return result }
|
||||
switch yAligningMode {
|
||||
case .min:
|
||||
break
|
||||
case .mid:
|
||||
result = result.move(
|
||||
dx: 0,
|
||||
dy: (rectToFitIn.h / 2 - newHeight / 2) / (newHeight / rect.h)
|
||||
)
|
||||
case .max:
|
||||
result = result.move(
|
||||
dx: 0,
|
||||
dy: (rectToFitIn.h - newHeight) / (newHeight / rect.h)
|
||||
)
|
||||
}
|
||||
let dx = xAligningMode.align(x: rectToFitIn.w, y: newWidth) / (newWidth / rect.w)
|
||||
let dy = yAligningMode.align(x: rectToFitIn.h, y: newHeight) / (newHeight / rect.h)
|
||||
result = result.move(dx: dx, dy: dy)
|
||||
|
||||
return result
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user