diff --git a/Sources/TokamakCore/Modifiers/Effects/ScaleEffect.swift b/Sources/TokamakCore/Modifiers/Effects/ScaleEffect.swift new file mode 100644 index 000000000..4fcc7b425 --- /dev/null +++ b/Sources/TokamakCore/Modifiers/Effects/ScaleEffect.swift @@ -0,0 +1,56 @@ +// Copyright 2020-2021 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 7/9/21. +// + +import Foundation + +@frozen public struct _ScaleEffect: GeometryEffect, Equatable { + public var scale: CGSize + public var anchor: UnitPoint + + @inlinable + public init(scale: CGSize, anchor: UnitPoint = .center) { + self.scale = scale + self.anchor = anchor + } + + public func effectValue(size: CGSize) -> ProjectionTransform { + .init(.init(scaleX: scale.width, y: scale.height)) + } + + public func body(content: Content) -> some View { + content + } +} + +public extension View { + @inlinable + func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View { + modifier(_ScaleEffect(scale: scale, anchor: anchor)) + } + + @inlinable + func scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some View { + scaleEffect(CGSize(width: s, height: s), anchor: anchor) + } + + @inlinable + func scaleEffect(x: CGFloat = 1.0, y: CGFloat = 1.0, + anchor: UnitPoint = .center) -> some View + { + scaleEffect(CGSize(width: x, height: y), anchor: anchor) + } +} diff --git a/Sources/TokamakStaticHTML/Modifiers/Effects/RotationEffect.swift b/Sources/TokamakStaticHTML/Modifiers/Effects/RotationEffect.swift index b48e30252..40a62ae8e 100644 --- a/Sources/TokamakStaticHTML/Modifiers/Effects/RotationEffect.swift +++ b/Sources/TokamakStaticHTML/Modifiers/Effects/RotationEffect.swift @@ -19,6 +19,11 @@ import TokamakCore extension _RotationEffect: DOMViewModifier { public var attributes: [HTMLAttribute: String] { - ["style": "transform: rotate(\(angle.degrees)deg)"] + [ + "style": """ + transform: rotate(\(angle.degrees)deg); + transform-origin: \(anchor.cssValue); + """, + ] } } diff --git a/Sources/TokamakStaticHTML/Modifiers/Effects/ScaleEffect.swift b/Sources/TokamakStaticHTML/Modifiers/Effects/ScaleEffect.swift new file mode 100644 index 000000000..a9c89141d --- /dev/null +++ b/Sources/TokamakStaticHTML/Modifiers/Effects/ScaleEffect.swift @@ -0,0 +1,29 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 7/9/21. +// + +import TokamakCore + +extension _ScaleEffect: DOMViewModifier { + public var attributes: [HTMLAttribute: String] { + [ + "style": """ + transform: scale(\(scale.width), \(scale.height)); + transform-origin: \(anchor.cssValue); + """, + ] + } +} diff --git a/Sources/TokamakStaticHTML/Tokens/Tokens.swift b/Sources/TokamakStaticHTML/Tokens/Tokens.swift index ecab78948..e809a244d 100644 --- a/Sources/TokamakStaticHTML/Tokens/Tokens.swift +++ b/Sources/TokamakStaticHTML/Tokens/Tokens.swift @@ -37,3 +37,9 @@ extension GridItem: CustomStringConvertible { } } } + +extension UnitPoint { + var cssValue: String { + "\(x * 100)% \((1 - y) * 100)%" + } +} diff --git a/Tests/TokamakStaticHTMLTests/RenderingTests.swift b/Tests/TokamakStaticHTMLTests/RenderingTests.swift index 7185fbb5d..efb0fb3aa 100644 --- a/Tests/TokamakStaticHTMLTests/RenderingTests.swift +++ b/Tests/TokamakStaticHTMLTests/RenderingTests.swift @@ -276,6 +276,54 @@ final class RenderingTests: XCTestCase { timeout: defaultSnapshotTimeout ) } + + func testScaleEffect() { + assertSnapshot( + matching: ZStack { + Circle() + .fill(Color.red) + .frame(width: 50, height: 50) + .scaleEffect(2) + .opacity(0.5) + Circle() + .fill(Color.blue) + .frame(width: 50, height: 50) + .opacity(0.5) + }, + as: .image(size: .init(width: 100, height: 100)), + timeout: defaultSnapshotTimeout + ) + } + + func testAnchoredModifiers() { + assertSnapshot( + matching: ZStack { + Circle() + .fill(Color.red) + .frame(width: 50, height: 50) + .scaleEffect(2, anchor: .topLeading) + .opacity(0.5) + Circle() + .fill(Color.blue) + .frame(width: 50, height: 50) + .scaleEffect(2, anchor: .center) + .opacity(0.5) + + Rectangle() + .fill(Color.red) + .frame(width: 50, height: 50) + .rotationEffect(.degrees(45), anchor: .topLeading) + .opacity(0.5) + Rectangle() + .fill(Color.blue) + .frame(width: 50, height: 50) + .rotationEffect(.degrees(45), anchor: .center) + .opacity(0.5) + }, + as: .image(size: .init(width: 200, height: 200)), + timeout: defaultSnapshotTimeout + ) + } } #endif diff --git a/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testAnchoredModifiers.1.png b/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testAnchoredModifiers.1.png new file mode 100644 index 000000000..40e7d7e78 Binary files /dev/null and b/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testAnchoredModifiers.1.png differ diff --git a/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testScaleEffect.1.png b/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testScaleEffect.1.png new file mode 100644 index 000000000..78c1c981d Binary files /dev/null and b/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testScaleEffect.1.png differ