Skip to content

Commit 77572fc

Browse files
authored
feat(core): Allow customizing origin of transforms (#1922)
1 parent 5a852b5 commit 77572fc

8 files changed

Lines changed: 166 additions & 11 deletions

File tree

crates/freya-core/src/data.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use crate::{
5050
text_height::TextHeightBehavior,
5151
text_overflow::TextOverflow,
5252
text_shadow::TextShadow,
53+
transform_origin::TransformOrigin,
5354
},
5455
};
5556

@@ -83,6 +84,7 @@ pub struct EffectData {
8384
pub overflow: Overflow,
8485
pub rotation: Option<f32>,
8586
pub scale: Option<Scale>,
87+
pub transform_origin: TransformOrigin,
8688
pub opacity: Option<f32>,
8789
pub blur: Option<f32>,
8890
pub scrollable: bool,
@@ -297,6 +299,8 @@ pub struct EffectState {
297299
pub scales: Rc<[NodeId]>,
298300
pub scale: Option<Scale>,
299301

302+
pub transform_origin: TransformOrigin,
303+
300304
pub opacities: Rc<[f32]>,
301305

302306
pub blur: Option<f32>,
@@ -320,6 +324,7 @@ impl EffectState {
320324
blur: None,
321325
rotation: None,
322326
scale: None,
327+
transform_origin: TransformOrigin::default(),
323328
..parent_effect_state.clone()
324329
};
325330

@@ -340,6 +345,7 @@ impl EffectState {
340345
if let Some(effect_data) = effect_data {
341346
self.overflow = effect_data.overflow;
342347
self.blur = effect_data.blur;
348+
self.transform_origin = effect_data.transform_origin;
343349

344350
if let Some(rotation) = effect_data.rotation {
345351
let mut rotations = parent_effect_state.rotations.to_vec();

crates/freya-core/src/elements/extensions.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ use crate::{
6767
text_height::TextHeightBehavior,
6868
text_overflow::TextOverflow,
6969
text_shadow::TextShadow,
70+
transform_origin::TransformOrigin,
7071
},
7172
};
7273

@@ -914,4 +915,12 @@ pub trait EffectExt: Sized {
914915
self.get_effect().scale = Some(scale.into());
915916
self
916917
}
918+
919+
/// Set the point that the scale and rotation effects pivot around.
920+
///
921+
/// Defaults to the element's center.
922+
fn transform_origin(mut self, transform_origin: impl Into<TransformOrigin>) -> Self {
923+
self.get_effect().transform_origin = transform_origin.into();
924+
self
925+
}
917926
}

crates/freya-core/src/elements/rect.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use crate::{
4545
Shadow,
4646
ShadowPosition,
4747
},
48+
transform_origin::TransformOrigin,
4849
},
4950
tree::DiffModifies,
5051
};
@@ -681,6 +682,17 @@ impl Rect {
681682
self
682683
}
683684

685+
/// Set the point that the scale and rotation effects pivot around.
686+
///
687+
/// Defaults to the element's center.
688+
pub fn transform_origin(mut self, transform_origin: impl Into<TransformOrigin>) -> Self {
689+
self.element
690+
.effect
691+
.get_or_insert_with(Default::default)
692+
.transform_origin = transform_origin.into();
693+
self
694+
}
695+
684696
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
685697
self.element
686698
.effect

crates/freya-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ pub mod prelude {
150150
text_height::*,
151151
text_overflow::*,
152152
text_shadow::*,
153+
transform_origin::*,
153154
vertical_align::*,
154155
},
155156
user_event::UserEvent,

crates/freya-core/src/render_pipeline.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ impl RenderPipeline<'_> {
5252
let layout_node = self.tree.layout.get(id).unwrap();
5353
let effect = self.tree.effect_state.get(id).unwrap();
5454
let area = layout_node.visible_area();
55-
let center = area.center();
55+
let origin = effect.transform_origin.origin(&area);
5656
let scale = effect.scale.unwrap();
5757

58-
visible_area = visible_area.translate(-center.to_vector());
58+
visible_area = visible_area.translate(-origin.to_vector());
5959
visible_area = visible_area.scale(scale.x, scale.y);
60-
visible_area = visible_area.translate(center.to_vector());
60+
visible_area = visible_area.translate(origin.to_vector());
6161
}
6262

6363
hotpath::measure_block!("Element Clipping", {
@@ -74,15 +74,15 @@ impl RenderPipeline<'_> {
7474
let scale_layout_node = self.tree.layout.get(id).unwrap();
7575
let scale_effect = self.tree.effect_state.get(id).unwrap();
7676
let area = scale_layout_node.visible_area();
77-
let center = area.center();
77+
let origin = scale_effect.transform_origin.origin(&area);
7878
let scale = scale_effect.scale.unwrap();
7979

8080
transformed_clip_area =
81-
transformed_clip_area.translate(-center.to_vector());
81+
transformed_clip_area.translate(-origin.to_vector());
8282
transformed_clip_area =
8383
transformed_clip_area.scale(scale.x, scale.y);
8484
transformed_clip_area =
85-
transformed_clip_area.translate(center.to_vector());
85+
transformed_clip_area.translate(origin.to_vector());
8686
}
8787

8888
// No need to render this element as it is completely clipped
@@ -106,12 +106,13 @@ impl RenderPipeline<'_> {
106106
let layout_node = self.tree.layout.get(id).unwrap();
107107
let effect = self.tree.effect_state.get(id).unwrap();
108108
let area = layout_node.visible_area();
109+
let origin = effect.transform_origin.origin(&area);
109110
let mut matrix = SkMatrix::new_identity();
110111
matrix.set_rotate(
111112
effect.rotation.unwrap(),
112113
Some(SkPoint {
113-
x: area.min_x() + area.width() / 2.0,
114-
y: area.min_y() + area.height() / 2.0,
114+
x: origin.x,
115+
y: origin.y,
115116
}),
116117
);
117118
self.canvas.concat(&matrix);
@@ -142,12 +143,12 @@ impl RenderPipeline<'_> {
142143
let layout_node = self.tree.layout.get(id).unwrap();
143144
let effect = self.tree.effect_state.get(id).unwrap();
144145
let area = layout_node.visible_area();
145-
let center = area.center();
146+
let origin = effect.transform_origin.origin(&area);
146147
let scale = effect.scale.unwrap();
147148

148-
self.canvas.translate((center.x, center.y));
149+
self.canvas.translate((origin.x, origin.y));
149150
self.canvas.scale((scale.x, scale.y));
150-
self.canvas.translate((-center.x, -center.y));
151+
self.canvas.translate((-origin.x, -origin.y));
151152
}
152153
}
153154

crates/freya-core/src/style/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ pub mod text_decoration;
1717
pub mod text_height;
1818
pub mod text_overflow;
1919
pub mod text_shadow;
20+
pub mod transform_origin;
2021
pub mod vertical_align;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use torin::prelude::{
2+
Area,
3+
Point2D,
4+
};
5+
6+
/// Position of a [`TransformOrigin`] along a single axis.
7+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8+
#[derive(Debug, Clone, Copy, PartialEq)]
9+
pub enum OriginValue {
10+
/// Fraction of the element's length along the axis, where `0.0` is the start
11+
/// and `1.0` is the end.
12+
Fraction(f32),
13+
/// Absolute pixels measured from the element's top-left corner.
14+
Pixels(f32),
15+
}
16+
17+
impl OriginValue {
18+
/// Resolve this value into an absolute offset given the element's length on the axis.
19+
fn resolve(self, length: f32) -> f32 {
20+
match self {
21+
OriginValue::Fraction(fraction) => length * fraction,
22+
OriginValue::Pixels(pixels) => pixels,
23+
}
24+
}
25+
}
26+
27+
/// Reference point that the scale and rotation effects of an element pivot around.
28+
///
29+
/// Defaults to the element's center.
30+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31+
#[derive(Debug, Clone, Copy, PartialEq)]
32+
pub struct TransformOrigin {
33+
pub x: OriginValue,
34+
pub y: OriginValue,
35+
}
36+
37+
impl Default for TransformOrigin {
38+
fn default() -> Self {
39+
Self::center()
40+
}
41+
}
42+
43+
impl TransformOrigin {
44+
/// Resolve the origin point in absolute coordinates for the given element area.
45+
pub fn origin(&self, area: &Area) -> Point2D {
46+
Point2D::new(
47+
area.min_x() + self.x.resolve(area.width()),
48+
area.min_y() + self.y.resolve(area.height()),
49+
)
50+
}
51+
52+
pub fn center() -> Self {
53+
Self {
54+
x: OriginValue::Fraction(0.5),
55+
y: OriginValue::Fraction(0.5),
56+
}
57+
}
58+
59+
pub fn top_left() -> Self {
60+
Self {
61+
x: OriginValue::Fraction(0.0),
62+
y: OriginValue::Fraction(0.0),
63+
}
64+
}
65+
66+
pub fn top() -> Self {
67+
Self {
68+
x: OriginValue::Fraction(0.5),
69+
y: OriginValue::Fraction(0.0),
70+
}
71+
}
72+
73+
pub fn top_right() -> Self {
74+
Self {
75+
x: OriginValue::Fraction(1.0),
76+
y: OriginValue::Fraction(0.0),
77+
}
78+
}
79+
80+
pub fn left() -> Self {
81+
Self {
82+
x: OriginValue::Fraction(0.0),
83+
y: OriginValue::Fraction(0.5),
84+
}
85+
}
86+
87+
pub fn right() -> Self {
88+
Self {
89+
x: OriginValue::Fraction(1.0),
90+
y: OriginValue::Fraction(0.5),
91+
}
92+
}
93+
94+
pub fn bottom_left() -> Self {
95+
Self {
96+
x: OriginValue::Fraction(0.0),
97+
y: OriginValue::Fraction(1.0),
98+
}
99+
}
100+
101+
pub fn bottom() -> Self {
102+
Self {
103+
x: OriginValue::Fraction(0.5),
104+
y: OriginValue::Fraction(1.0),
105+
}
106+
}
107+
108+
pub fn bottom_right() -> Self {
109+
Self {
110+
x: OriginValue::Fraction(1.0),
111+
y: OriginValue::Fraction(1.0),
112+
}
113+
}
114+
}
115+
116+
/// Shorthand for a fractional [`TransformOrigin`], where `0.0` is the start and `1.0` the end of each axis.
117+
impl From<(f32, f32)> for TransformOrigin {
118+
fn from((x, y): (f32, f32)) -> Self {
119+
Self {
120+
x: OriginValue::Fraction(x),
121+
y: OriginValue::Fraction(y),
122+
}
123+
}
124+
}

examples/feature_transform.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ fn app() -> impl IntoElement {
1616
.offset_y(25.)
1717
.scale(0.5)
1818
.rotate(45.)
19+
.transform_origin((0.35, 0.35))
1920
.child(
2021
rect()
2122
.font_size(50.)

0 commit comments

Comments
 (0)