1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-08-15 16:10:39 +03:00

WIP #561: Built-in zoom support

This commit is contained in:
Yuri Strot 2019-04-08 17:29:41 +07:00
parent 9a1abccbb6
commit 84602c48a0
9 changed files with 306 additions and 25 deletions

View File

@ -258,8 +258,9 @@
TargetAttributes = {
B02E75EC1C16104900D1971D = {
CreatedOnToolsVersion = 7.1.1;
DevelopmentTeam = FZXCM5CJ7P;
LastSwiftMigration = 1020;
ProvisioningStyle = Manual;
ProvisioningStyle = Automatic;
};
};
};
@ -502,7 +503,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEVELOPMENT_TEAM = "";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = FZXCM5CJ7P;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@ -521,13 +523,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEVELOPMENT_TEAM = "";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = FZXCM5CJ7P;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.exyte.Example.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;

View File

@ -234,6 +234,8 @@
5852891620B29D67003E51D1 /* TransformedLocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5852891520B29D67003E51D1 /* TransformedLocus.swift */; };
5852891720B29D67003E51D1 /* TransformedLocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5852891520B29D67003E51D1 /* TransformedLocus.swift */; };
5874CCB720DA8A860090DBD5 /* ColorMatrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5874CCB620DA8A860090DBD5 /* ColorMatrix.swift */; };
5876C63222572859000B31B6 /* MacawZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5876C63122572859000B31B6 /* MacawZoom.swift */; };
5876C63322572859000B31B6 /* MacawZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5876C63122572859000B31B6 /* MacawZoom.swift */; };
58944BDA20AC8A9A00657640 /* logo_base64.txt in Resources */ = {isa = PBXBuildFile; fileRef = 57B7A4E01EE70DA5009D78D7 /* logo_base64.txt */; };
58944BDB20AC8A9A00657640 /* clip.svg in Resources */ = {isa = PBXBuildFile; fileRef = C43B064C1F9738EF00787A35 /* clip.svg */; };
58B0523920E10E7100D45008 /* ColorMatrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5874CCB620DA8A860090DBD5 /* ColorMatrix.swift */; };
@ -676,6 +678,7 @@
585288F320AD96A2003E51D1 /* ContentLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentLayout.swift; sourceTree = "<group>"; };
5852891520B29D67003E51D1 /* TransformedLocus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformedLocus.swift; sourceTree = "<group>"; };
5874CCB620DA8A860090DBD5 /* ColorMatrix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorMatrix.swift; sourceTree = "<group>"; };
5876C63122572859000B31B6 /* MacawZoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacawZoom.swift; sourceTree = "<group>"; };
5B1A8C7520A15F7300E5FFAE /* SVGNodeLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGNodeLayout.swift; sourceTree = "<group>"; };
5B1AE18420B6A669007EECCB /* text-align-01-b-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "text-align-01-b-manual.svg"; sourceTree = "<group>"; };
5B1AE18520B6A669007EECCB /* paths-data-06-t-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "paths-data-06-t-manual.reference"; sourceTree = "<group>"; };
@ -1295,6 +1298,7 @@
57F108731F502A3600DC365B /* Touchable.swift */,
57E5E1501E3B393900D1CB28 /* MacawView.swift */,
57E5E1521E3B393900D1CB28 /* ShapeLayer.swift */,
5876C63122572859000B31B6 /* MacawZoom.swift */,
);
path = views;
sourceTree = "<group>";
@ -2082,6 +2086,7 @@
57614B3A1F83D15600875933 /* AnimationProducer.swift in Sources */,
585288F620AD96A2003E51D1 /* ContentLayout.swift in Sources */,
57614B3C1F83D15600875933 /* ShapeInterpolation.swift in Sources */,
5876C63322572859000B31B6 /* MacawZoom.swift in Sources */,
57614B3D1F83D15600875933 /* Graphics_iOS.swift in Sources */,
57614BDB1F8739EE00875933 /* MacawView+PDF.swift in Sources */,
57614B411F83D15600875933 /* Text.swift in Sources */,
@ -2222,6 +2227,7 @@
57A27BD51E44C5840057BD3A /* ShapeInterpolation.swift in Sources */,
A718CD471F45C28700966E06 /* Graphics_iOS.swift in Sources */,
57614BDA1F8739EE00875933 /* MacawView+PDF.swift in Sources */,
5876C63222572859000B31B6 /* MacawZoom.swift in Sources */,
57E5E1A21E3B393900D1CB28 /* Text.swift in Sources */,
57F1087C1F53CA7E00DC365B /* MDisplayLink_iOS.swift in Sources */,
57E5E1A61E3B393900D1CB28 /* RenderContext.swift in Sources */,

View File

@ -384,8 +384,8 @@ class AnimationContext {
func getLayoutTransform(_ renderer: NodeRenderer?) -> Transform {
if rootTransform == nil {
if let view = renderer?.view, let node = view.renderer?.node() {
rootTransform = LayoutHelper.calcTransform(node, view.contentLayout, view.bounds.size.toMacaw())
if let view = renderer?.view {
rootTransform = view.place
}
}
return rootTransform ?? Transform.identity

View File

@ -1,4 +1,4 @@
import Darwin
import Foundation
open class Arc: Locus {

View File

@ -1,3 +1,5 @@
import Foundation
open class Point: Locus {
public let x: Double
@ -11,24 +13,35 @@ open class Point: Locus {
}
override open func bounds() -> Rect {
return Rect(
x: x,
y: y,
w: 0.0,
h: 0.0)
return Rect(x: x, y: y, w: 0.0, h: 0.0)
}
open func add(_ point: Point) -> Point {
return Point(
x: self.x + point.x,
y: self.y + point.y)
return Point( x: x + point.x, y: y + point.y)
}
open func rect(size: Size) -> Rect {
return Rect(point: self, size: size)
}
open func distance(to point: Point) -> Double {
let dx = point.x - x
let dy = point.y - y
return sqrt(dx * dx + dy * dy)
}
override open func toPath() -> Path {
return MoveTo(x: x, y: y).lineTo(x: x, y: y).build()
}
}
extension Point: Equatable {
public static func == (lhs: Point, rhs: Point) -> Bool {
return lhs.x == rhs.x
&& lhs.y == rhs.y
}
public static func - (lhs: Point, rhs: Point) -> Size {
return Size(w: lhs.x - rhs.x, h: lhs.y - rhs.y)
}
}

View File

@ -1,3 +1,5 @@
import Foundation
open class Size {
public let w: Double
@ -13,11 +15,25 @@ open class Size {
open func rect(at point: Point = Point.origin) -> Rect {
return Rect(point: point, size: self)
}
open func angle() -> Double {
return atan2(h, w)
}
}
extension Size {
public static func == (lhs: Size, rhs: Size) -> Bool {
return lhs.w == rhs.w
&& lhs.h == rhs.h
return lhs.w == rhs.w && lhs.h == rhs.h
}
public static func + (lhs: Size, rhs: Size) -> Size {
return Size(w: lhs.w + rhs.w, h: lhs.h + rhs.h)
}
public static func - (lhs: Size, rhs: Size) -> Size {
return Size(w: lhs.w - rhs.w, h: lhs.h - rhs.h)
}
}

View File

@ -75,12 +75,19 @@ public final class Transform {
return Transform(m11: nm11, m12: nm12, m21: nm21, m22: nm22, dx: ndx, dy: ndy)
}
public func apply(to: Point) -> Point {
let x2 = m11 * to.x + m12 * to.x + dx
let y2 = m21 * to.y + m22 * to.y + dy
return Point(x: x2, y: y2)
}
public func invert() -> Transform? {
let det = self.m11 * self.m22 - self.m12 * self.m21
if det == 0 {
return nil
}
return Transform(m11: m22 / det, m12: -m12 / det, m21: -m21 / det, m22: m11 / det,
return Transform(m11: m22 / det, m12: -m12 / det,
m21: -m21 / det, m22: m11 / det,
dx: (m21 * dy - m22 * dx) / det,
dy: (m12 * dx - m11 * dy) / det)
}

View File

@ -5,6 +5,7 @@ import UIKit
#elseif os(OSX)
import AppKit
#endif
///
/// MacawView is a main class used to embed Macaw scene into your Cocoa UI.
/// You could create your own view extended from MacawView with predefined scene.
@ -43,6 +44,18 @@ open class MacawView: MView, MGestureRecognizerDelegate {
}
}
public let zoom = MacawZoom()
public var place: Transform {
return placeManager.placeVar.value
}
public var placeVar: Variable<Transform> {
return placeManager.placeVar
}
private let placeManager = RootPlaceManager()
override open var frame: CGRect {
didSet {
super.frame = frame
@ -106,6 +119,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
@objc public init?(node: Node, coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
zoom.initialize(view: self, onChange: onZoomChange)
initializeView()
@ -128,6 +142,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
public override init(frame: CGRect) {
super.init(frame: frame)
zoom.initialize(view: self, onChange: onZoomChange)
initializeView()
}
@ -136,6 +151,11 @@ open class MacawView: MView, MGestureRecognizerDelegate {
self.init(node: Group(), coder: aDecoder)
}
private func onZoomChange(t: Transform) {
placeManager.setZoom(place: t)
self.setNeedsDisplay()
}
func initializeView() {
self.contentLayout = .none
self.context = RenderContext(view: self)
@ -191,7 +211,11 @@ open class MacawView: MView, MGestureRecognizerDelegate {
return
}
renderer.calculateZPositionRecursively()
ctx.concatenate(layoutHelper.getTransform(renderer, contentLayout, bounds.size.toMacaw()))
// TODO: actually we should track all changes
placeManager.setLayout(place: layoutHelper.getTransform(renderer, contentLayout, bounds.size.toMacaw()))
ctx.concatenate(self.place.toCG())
renderer.render(in: ctx, force: false, opacity: node.opacity)
}
@ -210,7 +234,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
defer {
ctx.restoreGState()
}
let transform = layoutHelper.getTransform(renderer, contentLayout, bounds.size.toMacaw())
let transform = place.toCG()
ctx.concatenate(transform)
let loc = location.applying(transform.inverted())
return renderer.findNodeAt(parentNodePath: NodePath(node: Node(), location: loc), ctx: ctx)
@ -225,10 +249,29 @@ open class MacawView: MView, MGestureRecognizerDelegate {
return .none
}
open override func touchesBegan(_ touches: Set<MTouch>, with event: MEvent?) {
super.touchesBegan(touches, with: event)
zoom.touchesBegan(touches)
}
open override func touchesMoved(_ touches: Set<MTouch>, with event: MEvent?) {
super.touchesMoved(touches, with: event)
zoom.touchesMoved(touches)
}
open override func touchesEnded(_ touches: Set<MTouch>, with event: MEvent?) {
super.touchesEnded(touches, with: event)
zoom.touchesEnded(touches)
}
open override func touchesCancelled(_ touches: Set<MTouch>, with event: MEvent?) {
super.touchesCancelled(touches, with: event)
zoom.touchesEnded(touches)
}
// MARK: - Touches
override func mTouchesBegan(_ touches: [MTouchEvent]) {
if !self.node.shouldCheckForPressed() &&
!self.node.shouldCheckForMoved() &&
!self.node.shouldCheckForReleased () {
@ -547,9 +590,9 @@ class LayoutHelper {
private var prevSize: Size?
private var prevRect: Rect?
private var prevTransform: CGAffineTransform?
private var prevTransform: Transform?
public func getTransform(_ nodeRenderer: NodeRenderer, _ layout: ContentLayout, _ size: Size) -> CGAffineTransform {
public func getTransform(_ nodeRenderer: NodeRenderer, _ layout: ContentLayout, _ size: Size) -> Transform {
setSize(size: size)
let node = nodeRenderer.node()
var rect = node?.bounds
@ -566,9 +609,9 @@ class LayoutHelper {
if let transform = prevTransform {
return transform
}
return setTransform(transform: layout.layout(rect: prevRect!, into: size).toCG())
return setTransform(transform: layout.layout(rect: rect, into: size))
}
return CGAffineTransform.identity
return Transform.identity
}
public class func calcTransform(_ node: Node, _ layout: ContentLayout, _ size: Size) -> Transform {
@ -624,9 +667,39 @@ class LayoutHelper {
prevTransform = nil
}
private func setTransform(transform: CGAffineTransform) -> CGAffineTransform {
private func setTransform(transform: Transform) -> Transform {
prevTransform = transform
return transform
}
}
class RootPlaceManager {
var placeVar = Variable(Transform.identity)
private var places: [Transform] = [.identity, .identity]
func setLayout(place: Transform) {
if places[1] !== place {
places[1] = place
placeVar.value = recalc()
}
}
func setZoom(place: Transform) {
if places[0] !== place {
places[0] = place
placeVar.value = recalc()
}
}
private func recalc() -> Transform {
if places[0] === Transform.identity {
return places[1]
} else if places[1] === Transform.identity {
return places[0]
}
return places[0].concat(with: places[1])
}
}

View File

@ -0,0 +1,162 @@
//
// MacawZoom.swift
// Macaw
//
// Created by Yuri Strot on 4/5/19.
// Copyright © 2019 Exyte. All rights reserved.
//
import Foundation
#if os(iOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
open class MacawZoom {
private var view: MView!
private var onChange: ((Transform) -> Void)!
private var touches = [TouchData]()
private var zoomData = ZoomData()
private var trackMove = false
private var trackScale = false
private var trackRotate = false
open func enable(move: Bool = true, scale: Bool = true, rotate: Bool = false) {
trackMove = move
trackScale = scale
trackRotate = rotate
if scale || rotate {
view.isMultipleTouchEnabled = true
}
}
open func disable() {
trackMove = false
trackScale = false
trackRotate = false
}
open func set(offset: Size? = nil, scale: Double? = nil, angle: Double? = nil) {
let o = offset ?? zoomData.offset
let s = scale ?? zoomData.scale
let a = angle ?? zoomData.angle
zoomData = ZoomData(offset: o, scale: s, angle: a)
onChange(zoomData.transform())
}
func initialize(view: MView, onChange: @escaping ((Transform) -> Void)) {
self.view = view
self.onChange = onChange
}
func touchesBegan(_ touches: Set<MTouch>) {
zoomData = getNewZoom()
self.touches = self.touches.map { TouchData(touch: $0.touch, in: view) }
self.touches.append(contentsOf: touches.map { TouchData(touch: $0, in: view) })
}
func touchesMoved(_ touches: Set<MTouch>) {
let zoom = cleanTouches() ?? getNewZoom()
onChange(zoom.transform())
}
func touchesEnded(_ touches: Set<MTouch>) {
cleanTouches()
if let touch = touches.first {
if touches.count == 1 && touch.tapCount == 2 && touch.timestamp + 0.3 >= CACurrentMediaTime() {
set(offset: .zero, scale: 1, angle: 0)
}
}
}
@discardableResult private func cleanTouches() -> ZoomData? {
let newTouches = touches.filter { $0.touch.phase.rawValue < MTouch.Phase.ended.rawValue }
if newTouches.count != touches.count {
zoomData = getNewZoom()
touches = newTouches.map { TouchData(touch: $0.touch, in: view) }
return zoomData
}
return nil
}
private func getNewZoom() -> ZoomData {
if touches.count == 0 || (touches.count == 1 && !trackMove) {
return zoomData
}
let s1 = touches[0].point
let e1 = touches[0].current(in: view)
if touches.count == 1 {
return zoomData.move(delta: e1 - s1)
}
let s2 = touches[1].point
let e2 = touches[1].current(in: view)
let scale = trackScale ? e1.distance(to: e2) / s1.distance(to: s2) : 1
let a = trackRotate ? (e1 - e2).angle() - (s1 - s2).angle() : 0
var offset = Size.zero
if trackMove {
let sina = sin(a)
let cosa = cos(a)
let w = e1.x - scale * (s1.x * cosa - s1.y * sina)
let h = e1.y - scale * (s1.x * sina + s1.y * cosa)
offset = Size(w: w, h: h)
}
return ZoomData(offset: offset, scale: scale, angle: a).combine(with: zoomData)
}
}
fileprivate class ZoomData {
let offset: Size
let scale: Double
let angle: Double
init(offset: Size = Size.zero, scale: Double = 1, angle: Double = 0) {
self.offset = offset
self.scale = scale
self.angle = angle
}
func transform() -> Transform {
return Transform.move(dx: offset.w, dy: offset.h).scale(sx: scale, sy: scale).rotate(angle: angle)
}
func move(delta: Size) -> ZoomData {
return ZoomData(offset: offset + delta, scale: scale, angle: angle)
}
func combine(with: ZoomData) -> ZoomData {
let sina = sin(angle)
let cosa = cos(angle)
let w = offset.w + scale * (cosa * with.offset.w - sina * with.offset.h)
let h = offset.h + scale * (sina * with.offset.w + cosa * with.offset.h)
let s = scale * with.scale
let a = angle + with.angle
return ZoomData(offset: Size(w: w, h: h), scale: s, angle: a)
}
}
fileprivate class TouchData {
let touch: MTouch
let point: Point
convenience init(touch: MTouch, in view: MView) {
self.init(touch: touch, point: touch.location(in: view).toMacaw())
}
init(touch: MTouch, point: Point) {
self.touch = touch
self.point = point
}
func current(in view: MView) -> Point {
return touch.location(in: view).toMacaw()
}
}