1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// Simulated vertex shader logic for SVG point positioning.
use nalgebra_glm::{Vec2, Vec4, Mat4};
use crate::render_traits::{AspectRatioMode, AspectRatioAlignmentMode, UnitsMode};
pub fn get_scale_mat(x: f32, y: f32, z: f32) -> Mat4 {
return Mat4::from_columns(&[
Vec4::new(x, 0.0, 0.0, 0.0),
Vec4::new(0.0, y, 0.0, 0.0),
Vec4::new(0.0, 0.0, z, 0.0),
Vec4::new(0.0, 0.0, 0.0, 1.0)
]);
}
pub fn get_translate_mat(x: f32, y: f32, z: f32) -> Mat4 {
return Mat4::from_columns(&[
Vec4::new(1.0, 0.0, 0.0, 0.0),
Vec4::new(0.0, 1.0, 0.0, 0.0),
Vec4::new(0.0, 0.0, 1.0, 0.0),
Vec4::new(x, y, z, 1.0),
]);
}
pub fn get_aspect_ratio_mat(layer_aspect_ratio: f32, aspect_ratio_mode: AspectRatioMode, aspect_ratio_alignment_mode: AspectRatioAlignmentMode) -> Mat4 {
// Determine the x and y extents to use,
// based on the aspect ratio mode and layer aspect ratio.
// We only need to handle the aspect ratio mode when the layer_aspect_ratio is not 1.
let mut x_scale_for_aspect_ratio_mode = 1.0;
let mut y_scale_for_aspect_ratio_mode = 1.0;
if (aspect_ratio_mode == AspectRatioMode::Contain) {
// fit/contain
if (layer_aspect_ratio > 1.0) {
// Wide rectangle
// Show more than (0, 1) in x direction. Show exactly (0, 1) in y direction.
x_scale_for_aspect_ratio_mode = 1.0 / layer_aspect_ratio;
} else if(layer_aspect_ratio < 1.0) {
// Tall layer
// Show exactly (0, 1) in x direction. Show more than (0, 1) in y direction.
y_scale_for_aspect_ratio_mode = layer_aspect_ratio;
} else {
// Square layer; no change needed.
// Show exactly (0, 1) in both directions.
}
} else if (aspect_ratio_mode == AspectRatioMode::Cover) {
// fill/cover
if(layer_aspect_ratio > 1.0) {
// Wide rectangle
// Show exactly (0, 1) in x direction. Show less than (0, 1) in y direction.
y_scale_for_aspect_ratio_mode = layer_aspect_ratio;
} else if(layer_aspect_ratio < 1.0) {
// Tall layer
// Show less than (0, 1) in x direction. Show exactly (0, 1) in y direction.
x_scale_for_aspect_ratio_mode = 1.0 / layer_aspect_ratio;
} else {
// Square layer; no change needed.
// Show exactly (0, 1) in both directions.
}
}
// To handle aspect_ratio_alignment_mode, we compute the required translation.
let mut x_translation_for_aspect_ratio_alignment_mode = 0.0;
let mut y_translation_for_aspect_ratio_alignment_mode = 0.0;
if (aspect_ratio_alignment_mode == AspectRatioAlignmentMode::Start) {
// start
x_translation_for_aspect_ratio_alignment_mode = x_scale_for_aspect_ratio_mode - 1.0;
y_translation_for_aspect_ratio_alignment_mode = y_scale_for_aspect_ratio_mode - 1.0;
} else if (aspect_ratio_alignment_mode == AspectRatioAlignmentMode::End) {
// end
x_translation_for_aspect_ratio_alignment_mode = 1.0 - x_scale_for_aspect_ratio_mode;
y_translation_for_aspect_ratio_alignment_mode = 1.0 - y_scale_for_aspect_ratio_mode;
}
// Only scaling will result in the (0, 1) region being centered.
// If we want to align 0 to the left or bottom, we need to add a translation step as well.
return get_translate_mat(
x_translation_for_aspect_ratio_alignment_mode,
y_translation_for_aspect_ratio_alignment_mode,
0.0
) * get_scale_mat(
x_scale_for_aspect_ratio_mode,
y_scale_for_aspect_ratio_mode,
1.0
);
}
// TODO: get_margin_mat for handling of margins?
// (despite not needing to handle them in get_point_position)
// Here, we "simulate" the vertex shader logic in Rust,
// enabling us to check the logic that we are using for handling margins, aspect ratios, and camera transforms.
// It will require us to manually keep things in sync with the actual shader code, but that is ok.
// The rust syntax is luckily very similar to WGSL.
// Note: we treat the Y coordinate as increasing upwards, for consistency with the data coordinate system.
// Conversion to a coordinate system where Y increases downwards (e.g., for HTML canvas) is delegated to the caller.
pub fn get_point_position(
pos_x: f32,
pos_y: f32,
// "uniforms" below
layer_width_px: f32,
layer_height_px: f32,
camera_view_raw: &[f32],
data_unit_mode_x: UnitsMode,
data_unit_mode_y: UnitsMode,
aspect_ratio_mode: AspectRatioMode,
aspect_ratio_alignment_mode: AspectRatioAlignmentMode,
model_matrix_raw: Option<&[f32]>, // Column-major 4x4 model matrix (identity if None).
) -> (f32, f32) {
// Simulate the vertex shader logic here.
// Ideally, use the same variable names, and where possible, the same syntax.
// However, we want to output to pixel coordinates within the layer area.
let model_matrix = model_matrix_raw
.map(|m| Mat4::from_column_slice(m))
.unwrap_or(Mat4::identity());
let mut pixel_output: (f32, f32) = (0.0, 0.0);
if data_unit_mode_x == UnitsMode::Pixels || data_unit_mode_y == UnitsMode::Pixels {
// Pixel units mode: model_matrix is applied in normalized (0,1) space.
// Matches the shader logic:
// point_pos_norm = vertex_pos_px / layer_size
// point_pos_ndc = NORM_TO_NDC_MAT * model_matrix * vec4(point_pos_norm, 0, 1)
let pos_norm = Vec4::new(
pos_x / layer_width_px,
pos_y / layer_height_px,
0.0, 1.0
);
let pos_transformed = model_matrix * pos_norm;
pixel_output = (pos_transformed.x * layer_width_px, pos_transformed.y * layer_height_px);
if data_unit_mode_x == UnitsMode::Pixels && data_unit_mode_y == UnitsMode::Pixels {
return pixel_output;
}
}
let camera_view = Mat4::from_column_slice(camera_view_raw);
let layer_aspect_ratio = layer_width_px / layer_height_px;
// Get the scale() matrix to handle the aspect ratio mode.
let ASPECT_RATIO_MAT = get_aspect_ratio_mat(
layer_aspect_ratio,
aspect_ratio_mode,
aspect_ratio_alignment_mode
);
// We operate in (0 to 1) space, since it is more intuitive.
// We therefore need matrices to transform (0, 1) into clip space ("NDC") (-1 to 1)
let NORM_TO_NDC_MAT = get_translate_mat(-1.0, -1.0, 0.0) * get_scale_mat(2.0, 2.0, 1.0); // Scale up by 2, THEN translate by -1 (i.e., translating in the scaled-up space)
// And the inverse, to convert back from NDC (-1 to 1) to normalized (0 to 1) space.
let NDC_TO_NORM_MAT = get_translate_mat(0.5, 0.5, 0.0) * get_scale_mat(0.5, 0.5, 1.0); // Scale down by 0.5, THEN translate by 0.5 (i.e., translating in the scaled-down space)
// Model-view-projection matrix
// References:
// - https://github.com/flekschas/regl-scatterplot/blob/17a650c352fad313d1574472b2fdc5f58b9e1eca/src/index.js#L1582
// - https://nalgebra.rs/docs/user_guide/cg_recipes#build-a-mvp-matrix
let model_view_projection = ASPECT_RATIO_MAT * camera_view;
// TYPICALLY: position = projectionMatrix * viewMatrix * modelMatrix * inputModelSpacePosition
// Where:
// - inputPosition - the 4D vertex position (homogeneous coordinate) in model space.
// - modelMatrix - the 4x4 matrix that transforms input vertices from model space to world space.
// - viewMatrix - the 4x4 view matrix, which takes as input a point in world space and the result is a point in camera space.
// - projectionMatrix - the 4x4 projection matrix, which takes as input a point in camera space and the result is a projected point in clip space.
let point_pos_norm = /*LAYER_NORM_TO_VIEW_NORM_MAT * */ (
// The camera from dom-2d-camera operates in NDC space.
// The `dom-2d-camera` library is designed to work in **NDC space (-1 to 1)**, not normalized space (0 to 1).
// When you zoom in, the scale increases, and when you pan, the translation values are in NDC space.
// However, after this transformation, we want to be working in (0 to 1) normalized space.
// The camera operates in NDC space, but your data is in normalized space. We need to:
// 1. Convert data from (0,1) to NDC (-1,1)
// 2. Apply camera
// 3. Convert back to (0,1)
// 4. Apply aspect ratio and margins
// 5. Convert final result to NDC for rendering
// We apply camera AFTER converting to NDC, and DON'T convert back until
// after all NDC-space operations are done. This keeps translations in the correct space.
(NDC_TO_NORM_MAT * model_view_projection * NORM_TO_NDC_MAT)
// The model_matrix transforms coordinates in model space before the camera is applied,
// to allow for applying user-provided affine transformations.
* model_matrix * Vec4::new(pos_x, pos_y, 0.0, 1.0)
);
// Matrix to convert from normalized (0 to 1) space to pixel space.
// Note: the SVG coordinate system has (0,0) at the top-left,
// with +X to the right and +Y downwards, so we also need to flip the Y axis.
let NORM_TO_PX_MAT = get_scale_mat(
layer_width_px,
layer_height_px,
1.0
);
let point_pos_px = NORM_TO_PX_MAT * Vec4::new(point_pos_norm.x, point_pos_norm.y, 0.0, 1.0);
let output_x = if data_unit_mode_x == UnitsMode::Pixels { pixel_output.0 } else { point_pos_px.x };
let output_y = if data_unit_mode_y == UnitsMode::Pixels { pixel_output.1 } else { point_pos_px.y };
// Don't flip the Y coordinate here, and instead delegate to the caller if flipping is required.
return (output_x, output_y);
}
// Compute how a size (width, height) transforms through the same pipeline as positions.
// A size is the difference between two positions, so translations cancel out (w=0).
// This is useful for determining, e.g., how large an image appears after camera/aspect-ratio transforms.
pub fn get_point_size(
size_x: f32,
size_y: f32,
// "uniforms" below
layer_width_px: f32,
layer_height_px: f32,
camera_view_raw: &[f32],
data_unit_mode_x: UnitsMode,
data_unit_mode_y: UnitsMode,
aspect_ratio_mode: AspectRatioMode,
aspect_ratio_alignment_mode: AspectRatioAlignmentMode,
model_matrix_raw: Option<&[f32]>,
) -> (f32, f32) {
let model_matrix = model_matrix_raw
.map(|m| Mat4::from_column_slice(m))
.unwrap_or(Mat4::identity());
let mut pixel_output = (0.0_f32, 0.0_f32);
if data_unit_mode_x == UnitsMode::Pixels || data_unit_mode_y == UnitsMode::Pixels {
// Pixel mode: model_matrix applied in normalized space (w=0 for size).
let size_norm = Vec4::new(
size_x / layer_width_px,
size_y / layer_height_px,
0.0, 0.0
);
let size_transformed = model_matrix * size_norm;
pixel_output = (size_transformed.x * layer_width_px, size_transformed.y * layer_height_px);
if data_unit_mode_x == UnitsMode::Pixels && data_unit_mode_y == UnitsMode::Pixels {
return pixel_output;
}
}
let camera_view = Mat4::from_column_slice(camera_view_raw);
let layer_aspect_ratio = layer_width_px / layer_height_px;
let ASPECT_RATIO_MAT = get_aspect_ratio_mat(
layer_aspect_ratio,
aspect_ratio_mode,
aspect_ratio_alignment_mode
);
let NORM_TO_NDC_MAT = get_translate_mat(-1.0, -1.0, 0.0) * get_scale_mat(2.0, 2.0, 1.0);
let NDC_TO_NORM_MAT = get_translate_mat(0.5, 0.5, 0.0) * get_scale_mat(0.5, 0.5, 1.0);
let model_view_projection = ASPECT_RATIO_MAT * camera_view;
// Use w=0: translations cancel out for sizes (deltas between two positions).
let size_norm = (NDC_TO_NORM_MAT * model_view_projection * NORM_TO_NDC_MAT)
* model_matrix * Vec4::new(size_x, size_y, 0.0, 0.0);
let NORM_TO_PX_MAT = get_scale_mat(
layer_width_px,
layer_height_px,
1.0
);
let size_px = NORM_TO_PX_MAT * Vec4::new(size_norm.x, size_norm.y, 0.0, 0.0);
let output_x = if data_unit_mode_x == UnitsMode::Pixels { pixel_output.0 } else { size_px.x };
let output_y = if data_unit_mode_y == UnitsMode::Pixels { pixel_output.1 } else { size_px.y };
return (output_x, output_y);
}