1#[derive(Debug, Clone, PartialEq)]
13pub enum TransformOp {
14 Rotate(f32),
15 RotateX(f32),
16 RotateY(f32),
17 RotateZ(f32),
18 Rotate3d(f32, f32, f32, f32),
19 Scale(f32),
20 ScaleX(f32),
21 ScaleY(f32),
22 ScaleZ(f32),
23 Scale3d(f32, f32, f32),
24 TranslateX(f32),
25 TranslateY(f32),
26 TranslateZ(f32),
27 Translate(f32, f32),
28 Translate3d(f32, f32, f32),
29 SkewX(f32),
30 SkewY(f32),
31 Skew(f32, f32),
32 Perspective(f32),
33 Matrix(f32, f32, f32, f32, f32, f32),
34 Matrix3d([f32; 16]),
35}
36
37impl TransformOp {
38 pub fn to_css(&self) -> String {
39 match self {
40 Self::Rotate(deg) => format!("rotate({deg}deg)"),
41 Self::RotateX(deg) => format!("rotateX({deg}deg)"),
42 Self::RotateY(deg) => format!("rotateY({deg}deg)"),
43 Self::RotateZ(deg) => format!("rotateZ({deg}deg)"),
44 Self::Rotate3d(x, y, z, d) => format!("rotate3d({x}, {y}, {z}, {d}deg)"),
45 Self::Scale(s) => format!("scale({s})"),
46 Self::ScaleX(s) => format!("scaleX({s})"),
47 Self::ScaleY(s) => format!("scaleY({s})"),
48 Self::ScaleZ(s) => format!("scaleZ({s})"),
49 Self::Scale3d(x, y, z) => format!("scale3d({x}, {y}, {z})"),
50 Self::TranslateX(px) => format!("translateX({px}px)"),
51 Self::TranslateY(px) => format!("translateY({px}px)"),
52 Self::TranslateZ(px) => format!("translateZ({px}px)"),
53 Self::Translate(x, y) => format!("translate({x}px, {y}px)"),
54 Self::Translate3d(x, y, z) => format!("translate3d({x}px, {y}px, {z}px)"),
55 Self::SkewX(deg) => format!("skewX({deg}deg)"),
56 Self::SkewY(deg) => format!("skewY({deg}deg)"),
57 Self::Skew(x, y) => format!("skew({x}deg, {y}deg)"),
58 Self::Perspective(px) => format!("perspective({px}px)"),
59 Self::Matrix(a, b, c, d, e, f) => format!("matrix({a}, {b}, {c}, {d}, {e}, {f})"),
60 Self::Matrix3d(m) => {
61 let vals: Vec<String> = m.iter().map(|v| v.to_string()).collect();
62 format!("matrix3d({})", vals.join(", "))
63 }
64 }
65 }
66}
67
68pub fn rotate(deg: f32) -> TransformOp {
69 TransformOp::Rotate(deg)
70}
71pub fn rotate_x(deg: f32) -> TransformOp {
72 TransformOp::RotateX(deg)
73}
74pub fn rotate_y(deg: f32) -> TransformOp {
75 TransformOp::RotateY(deg)
76}
77pub fn rotate_z(deg: f32) -> TransformOp {
78 TransformOp::RotateZ(deg)
79}
80pub fn rotate3d(x: f32, y: f32, z: f32, deg: f32) -> TransformOp {
81 TransformOp::Rotate3d(x, y, z, deg)
82}
83pub fn scale(s: f32) -> TransformOp {
84 TransformOp::Scale(s)
85}
86pub fn scale_x(s: f32) -> TransformOp {
87 TransformOp::ScaleX(s)
88}
89pub fn scale_y(s: f32) -> TransformOp {
90 TransformOp::ScaleY(s)
91}
92pub fn scale_z(s: f32) -> TransformOp {
93 TransformOp::ScaleZ(s)
94}
95pub fn scale3d(x: f32, y: f32, z: f32) -> TransformOp {
96 TransformOp::Scale3d(x, y, z)
97}
98pub fn translate_x(px: f32) -> TransformOp {
99 TransformOp::TranslateX(px)
100}
101pub fn translate_y(px: f32) -> TransformOp {
102 TransformOp::TranslateY(px)
103}
104pub fn translate_z(px: f32) -> TransformOp {
105 TransformOp::TranslateZ(px)
106}
107pub fn translate(x: f32, y: f32) -> TransformOp {
108 TransformOp::Translate(x, y)
109}
110pub fn translate3d(x: f32, y: f32, z: f32) -> TransformOp {
111 TransformOp::Translate3d(x, y, z)
112}
113pub fn skew_x(deg: f32) -> TransformOp {
114 TransformOp::SkewX(deg)
115}
116pub fn skew_y(deg: f32) -> TransformOp {
117 TransformOp::SkewY(deg)
118}
119pub fn skew(x: f32, y: f32) -> TransformOp {
120 TransformOp::Skew(x, y)
121}
122pub fn perspective(px: f32) -> TransformOp {
123 TransformOp::Perspective(px)
124}
125pub fn matrix(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> TransformOp {
126 TransformOp::Matrix(a, b, c, d, e, f)
127}
128pub fn matrix3d(m: [f32; 16]) -> TransformOp {
129 TransformOp::Matrix3d(m)
130}
131
132pub fn make_transform(ops: &[TransformOp]) -> String {
135 ops.iter()
136 .map(|op| op.to_css())
137 .collect::<Vec<_>>()
138 .join(" ")
139}
140
141#[derive(Debug, Clone, PartialEq)]
145pub enum StyleValue {
146 String(String),
147 Px(f64),
148 Percent(f64),
149 Number(f64),
150}
151
152impl StyleValue {
153 pub fn to_css_string(&self) -> String {
154 match self {
155 Self::String(s) => s.clone(),
156 Self::Px(n) => format!("{n}px"),
157 Self::Percent(n) => format!("{n}%"),
158 Self::Number(n) => n.to_string(),
159 }
160 }
161}
162
163pub type StyleMap = std::collections::HashMap<String, StyleValue>;
165
166pub fn interpolate_styles(input: f64, input_range: &[f64], output_styles: &[StyleMap]) -> StyleMap {
169 assert!(
170 input_range.len() >= 2,
171 "input_range must have at least 2 entries"
172 );
173 assert_eq!(input_range.len(), output_styles.len());
174
175 let clamped = input.clamp(input_range[0], *input_range.last().unwrap());
176 let mut seg = input_range.len() - 2;
177 for i in 0..input_range.len() - 1 {
178 if clamped <= input_range[i + 1] {
179 seg = i;
180 break;
181 }
182 }
183
184 let t0 = input_range[seg];
185 let t1 = input_range[seg + 1];
186 let t = if (t1 - t0).abs() < 1e-12 {
187 1.0
188 } else {
189 (clamped - t0) / (t1 - t0)
190 };
191
192 let left = &output_styles[seg];
193 let right = &output_styles[seg + 1];
194 let mut result = StyleMap::new();
195 let keys: std::collections::HashSet<&String> = left.keys().chain(right.keys()).collect();
196
197 for key in keys {
198 let lv = left.get(key);
199 let rv = right.get(key);
200 let v = match (lv, rv) {
201 (Some(StyleValue::Number(a)), Some(StyleValue::Number(b))) => {
202 StyleValue::Number(a + (b - a) * t)
203 }
204 (Some(StyleValue::Px(a)), Some(StyleValue::Px(b))) => StyleValue::Px(a + (b - a) * t),
205 (Some(StyleValue::Percent(a)), Some(StyleValue::Percent(b))) => {
206 StyleValue::Percent(a + (b - a) * t)
207 }
208 (Some(v), _) => v.clone(),
209 (None, Some(v)) => v.clone(),
210 (None, None) => continue,
211 };
212 result.insert(key.clone(), v);
213 }
214 result
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn rotate_css() {
223 assert_eq!(rotate(45.0).to_css(), "rotate(45deg)");
224 }
225 #[test]
226 fn scale_css() {
227 assert_eq!(scale(1.5).to_css(), "scale(1.5)");
228 }
229 #[test]
230 fn translate_x_css() {
231 assert_eq!(translate_x(100.0).to_css(), "translateX(100px)");
232 }
233 #[test]
234 fn translate_css() {
235 assert_eq!(translate(10.0, 20.0).to_css(), "translate(10px, 20px)");
236 }
237 #[test]
238 fn skew_x_css() {
239 assert_eq!(skew_x(15.0).to_css(), "skewX(15deg)");
240 }
241 #[test]
242 fn perspective_css() {
243 assert_eq!(perspective(800.0).to_css(), "perspective(800px)");
244 }
245 #[test]
246 fn rotate3d_css() {
247 assert_eq!(
248 rotate3d(0.0, 1.0, 0.0, 45.0).to_css(),
249 "rotate3d(0, 1, 0, 45deg)"
250 );
251 }
252 #[test]
253 fn translate3d_css() {
254 assert_eq!(
255 translate3d(10.0, 20.0, 30.0).to_css(),
256 "translate3d(10px, 20px, 30px)"
257 );
258 }
259 #[test]
260 fn scale3d_css() {
261 assert_eq!(scale3d(2.0, 1.0, 0.5).to_css(), "scale3d(2, 1, 0.5)");
262 }
263 #[test]
264 fn matrix_css() {
265 assert_eq!(
266 matrix(1.0, 0.0, 0.0, 1.0, 50.0, 100.0).to_css(),
267 "matrix(1, 0, 0, 1, 50, 100)"
268 );
269 }
270
271 #[test]
272 fn make_transform_empty() {
273 assert_eq!(make_transform(&[]), "");
274 }
275
276 #[test]
277 fn make_transform_chain() {
278 assert_eq!(
279 make_transform(&[rotate(45.0), scale(1.5), translate_x(100.0)]),
280 "rotate(45deg) scale(1.5) translateX(100px)"
281 );
282 }
283
284 #[test]
285 fn make_transform_3d() {
286 assert_eq!(
287 make_transform(&[
288 perspective(500.0),
289 rotate_y(30.0),
290 translate3d(0.0, 0.0, -100.0)
291 ]),
292 "perspective(500px) rotateY(30deg) translate3d(0px, 0px, -100px)"
293 );
294 }
295
296 #[test]
297 fn interpolate_styles_midpoint() {
298 let kf0: StyleMap = [("opacity".to_string(), StyleValue::Number(0.0))].into();
299 let kf1: StyleMap = [("opacity".to_string(), StyleValue::Number(1.0))].into();
300 let r = interpolate_styles(15.0, &[0.0, 30.0], &[kf0, kf1]);
301 if let Some(StyleValue::Number(v)) = r.get("opacity") {
302 assert!((v - 0.5).abs() < 1e-9);
303 } else {
304 panic!("opacity missing");
305 }
306 }
307
308 #[test]
309 fn interpolate_styles_clamps() {
310 let kf0: StyleMap = [("o".to_string(), StyleValue::Number(0.0))].into();
311 let kf1: StyleMap = [("o".to_string(), StyleValue::Number(1.0))].into();
312 let lo = interpolate_styles(-999.0, &[0.0, 30.0], &[kf0.clone(), kf1.clone()]);
313 let hi = interpolate_styles(999.0, &[0.0, 30.0], &[kf0, kf1]);
314 if let Some(StyleValue::Number(v)) = lo.get("o") {
315 assert!((v - 0.0).abs() < 1e-9);
316 }
317 if let Some(StyleValue::Number(v)) = hi.get("o") {
318 assert!((v - 1.0).abs() < 1e-9);
319 }
320 }
321
322 #[test]
323 fn interpolate_styles_px() {
324 let kf0: StyleMap = [("width".to_string(), StyleValue::Px(100.0))].into();
325 let kf1: StyleMap = [("width".to_string(), StyleValue::Px(200.0))].into();
326 let r = interpolate_styles(0.5, &[0.0, 1.0], &[kf0, kf1]);
327 if let Some(StyleValue::Px(v)) = r.get("width") {
328 assert!((v - 150.0).abs() < 1e-9);
329 }
330 }
331
332 #[test]
333 fn interpolate_styles_multi_segment() {
334 let kf0: StyleMap = [("o".to_string(), StyleValue::Number(0.0))].into();
335 let kf1: StyleMap = [("o".to_string(), StyleValue::Number(0.5))].into();
336 let kf2: StyleMap = [("o".to_string(), StyleValue::Number(1.0))].into();
337 let r = interpolate_styles(45.0, &[0.0, 30.0, 60.0], &[kf0, kf1, kf2]);
338 if let Some(StyleValue::Number(v)) = r.get("o") {
339 assert!((v - 0.75).abs() < 1e-9);
340 }
341 }
342
343 #[test]
344 fn style_value_css_strings() {
345 assert_eq!(StyleValue::Number(1.0).to_css_string(), "1");
346 assert_eq!(StyleValue::Px(32.5).to_css_string(), "32.5px");
347 assert_eq!(StyleValue::Percent(50.0).to_css_string(), "50%");
348 assert_eq!(StyleValue::String("auto".into()).to_css_string(), "auto");
349 }
350}