Skip to content

Commit

Permalink
Support rule cap support
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmmease committed Jan 12, 2024
1 parent 3b5ac4f commit d407dad
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"width": 210,
"height": 215,
"origin_x": 5,
"origin_y": 10
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"marktype": "group",
"name": "root",
"role": "frame",
"interactive": true,
"clip": false,
"items": [
{
"items": [
{
"marktype": "rule",
"name": "marks",
"role": "mark",
"interactive": true,
"clip": false,
"items": [
{
"x": 140,
"y": 15,
"opacity": 0.5,
"stroke": "orange",
"strokeWidth": 12,
"strokeCap": "butt",
"x2": 60,
"y2": 180
},
{
"x": 20,
"y": 15,
"opacity": 0.5,
"stroke": "blue",
"strokeWidth": 16,
"strokeCap": "square",
"x2": 120,
"y2": 140
},
{
"x": 50,
"y": 15,
"opacity": 0.5,
"stroke": "green",
"strokeWidth": 20,
"strokeCap": "round",
"x2": 120,
"y2": 180
}
],
"zindex": 0
}
],
"x": 0,
"y": 0,
"width": 200,
"height": 200,
"fill": "transparent",
"stroke": "transparent"
}
],
"zindex": 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"data": [{
"name": "source_0",
"values": [
{"x": 140, "x2": 60, "y": 15, "y2": 180, "fill": "orange"},
{"x": 20, "x2": 120, "y": 15, "y2": 140, "fill": "blue"},
{"x": 50, "x2": 120, "y": 15, "y2": 180, "fill": "green"}
{"x": 140, "x2": 60, "y": 15, "y2": 180, "fill": "orange", "cap": "butt", "width": 12},
{"x": 20, "x2": 120, "y": 15, "y2": 140, "fill": "blue", "cap": "square", "width": 16},
{"x": 50, "x2": 120, "y": 15, "y2": 180, "fill": "green", "cap": "round", "width": 20}
]
}],
"marks": [
Expand All @@ -23,14 +23,14 @@
"from": {"data": "source_0"},
"encode": {
"update": {
"strokeWidth": {"value": 16},
"strokeWidth": {"field": "width"},
"stroke": {"field": "fill"},
"x": {"field": "x" },
"x2": {"field": "x2"},
"y": {"field": "y"},
"y2": {"field": "y2"},
"opacity": {"value": 0.5},
"strokeCap": {"value": "butt"}
"strokeCap": {"field": "cap"}
}
}
}
Expand Down
17 changes: 15 additions & 2 deletions sg2d-wgpu/src/marks/rule.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::marks::mark::MarkShader;
use itertools::izip;
use sg2d::marks::rule::RuleMark;
use sg2d::value::StrokeCap;
use wgpu::VertexBufferLayout;

#[repr(C)]
Expand All @@ -23,6 +24,10 @@ impl RuleVertex {
}
}

const STROKE_CAP_BUTT: u32 = 0;
const STROKE_CAP_SQUARE: u32 = 1;
const STROKE_CAP_ROUND: u32 = 2;

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RuleInstance {
Expand All @@ -32,15 +37,17 @@ pub struct RuleInstance {
pub y1: f32,
pub stroke: [f32; 4],
pub stroke_width: f32,
pub stroke_cap: u32,
}

const INSTANCE_ATTRIBUTES: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
const INSTANCE_ATTRIBUTES: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
1 => Float32, // x0
2 => Float32, // y0
3 => Float32, // x1
4 => Float32, // y1
5 => Float32x4, // stroke
6 => Float32, // stroke_width
7 => Uint32, // stroke_cap_type
];

impl RuleInstance {
Expand All @@ -52,14 +59,20 @@ impl RuleInstance {
mark.y1_iter(),
mark.stroke_iter(),
mark.stroke_width_iter(),
mark.stroke_cap_iter(),
)
.map(|(x0, y0, x1, y1, stroke, stroke_width)| RuleInstance {
.map(|(x0, y0, x1, y1, stroke, stroke_width, cap)| RuleInstance {
x0: *x0,
y0: *y0,
x1: *x1,
y1: *y1,
stroke: *stroke,
stroke_width: *stroke_width,
stroke_cap: match cap {
StrokeCap::Butt => STROKE_CAP_BUTT,
StrokeCap::Square => STROKE_CAP_SQUARE,
StrokeCap::Round => STROKE_CAP_ROUND,
},
})
}
}
Expand Down
95 changes: 90 additions & 5 deletions sg2d-wgpu/src/marks/rule.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,24 @@ struct InstanceInput {
@location(4) y1: f32,
@location(5) stroke: vec4<f32>,
@location(6) stroke_width: f32,
@location(7) stroke_cap: u32,
};

struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) p0: vec2<f32>,
@location(2) p1: vec2<f32>,
@location(3) radius: f32,
@location(4) stroke_half_width: f32,
};

const PI = 3.14159265359;

const STROKE_CAP_BUTT: u32 = 0u;
const STROKE_CAP_SQUARE: u32 = 1u;
const STROKE_CAP_ROUND: u32 = 2u;

@vertex
fn vs_main(
model: VertexInput,
Expand All @@ -37,11 +46,29 @@ fn vs_main(
var out: VertexOutput;
out.color = instance.stroke;

let p0 = vec2(instance.x0, instance.y0);
let p1 = vec2(instance.x1, instance.y1);
var width: f32 = instance.stroke_width;
var p0: vec2<f32> = vec2(instance.x0, instance.y0);
var p1: vec2<f32> = vec2(instance.x1, instance.y1);
let mid = (p0 + p1) / 2.0;
let len = distance(p0, p1);
let width = instance.stroke_width;
var len: f32 = distance(p0, p1);
if (instance.stroke_cap == STROKE_CAP_ROUND) {
// extend length, but leave p0 and p1 at the center of the circleular end caps
len += width;
} else if (instance.stroke_cap == STROKE_CAP_SQUARE) {
// Extend length and move p0 and p1 to the outer edge of the square
len += width;
let p0p1_norm = normalize(p1 - p0);
p0 -= p0p1_norm * (width / 2.0);
p1 += p0p1_norm * (width / 2.0);
}

let should_anitalias = instance.stroke_cap == STROKE_CAP_ROUND || (instance.x0 != instance.x1 && instance.y0 != instance.y1);
if (should_anitalias) {
// Add anti-aliasing buffer for rules with rounded caps and all diagonal rules.
// Non-round rules that are vertical or horizontal don't get anti-aliasing.
len += chart_uniforms.scale;
width += chart_uniforms.scale;
}

let normed = normalize(p1 - p0);
let angle = (PI / 2.0) + atan2(normed[1], normed[0]);
Expand All @@ -52,11 +79,69 @@ fn vs_main(
let y = 2.0 * (rot_pos[1] + (chart_uniforms.size[1] - mid[1])) / chart_uniforms.size[1] - 1.0;

out.clip_position = vec4<f32>(x, y, 0.0, 1.0);

out.p0 = p0 * chart_uniforms.scale;
out.p1 = p1 * chart_uniforms.scale;

out.stroke_half_width = instance.stroke_width * chart_uniforms.scale / 2.0;
if (instance.stroke_cap == STROKE_CAP_ROUND) {
out.radius = instance.stroke_width * chart_uniforms.scale / 2.0;
} else {
out.radius = 0.0;
}

return out;
}

// Fragment shader
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return in.color;
let should_antialias = in.radius > 0.0 || (in.p0[0] != in.p1[0] && in.p0[1] != in.p1[1]);
if (!should_antialias) {
// This is a butt or square cap and fully vertical or horizontal
// vertex boundary matches desired rule area and we don't need to do any
// anti-aliasing.
return in.color;
}

let frag_pos = vec2<f32>(in.clip_position[0], in.clip_position[1]);
let relative_frag_pos = frag_pos - in.p0;

let relative_p1 = in.p1 - in.p0;
let relative_p0 = vec2<f32>(0.0, 0.0);

let len = length(relative_p1);
let projected_frag_dist = dot(relative_frag_pos, relative_p1) / length(relative_p1);
let perpendicular_frag_pos = relative_frag_pos - normalize(relative_p1) * projected_frag_dist;

// Compute fragment distance for anit-aliasing
var dist: f32 = 0.0;
if (in.radius > 0.0) {
// rounded cap
if (projected_frag_dist < 0.0) {
// distance to p0
dist = distance(relative_frag_pos, relative_p0);
} else if (projected_frag_dist > len) {
// distance to p1
dist = distance(relative_frag_pos, relative_p1);
} else {
// distance to line connecting p0 and p1
dist = length(perpendicular_frag_pos);
}
} else {
// rule square or butt cap on a diagonal
if (projected_frag_dist < in.stroke_half_width) {
dist = max(-projected_frag_dist + in.stroke_half_width, length(perpendicular_frag_pos));
} else if (projected_frag_dist > len - in.stroke_half_width) {
dist = max(projected_frag_dist - len + in.stroke_half_width, length(perpendicular_frag_pos));
} else {
dist = length(perpendicular_frag_pos);
}
}

let buffer = chart_uniforms.scale / 2.0;
let alpha_factor = 1.0 - smoothstep(in.stroke_half_width - buffer, in.stroke_half_width + buffer, dist);
var adjusted_color = in.color;
adjusted_color[3] *= alpha_factor;
return adjusted_color;
}
58 changes: 29 additions & 29 deletions sg2d-wgpu/tests/test_image_baselines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,35 @@ mod test_image_baselines {
category,
spec_name,
tolerance,
// case("rect", "stacked_bar", 0.001),
// case("rect", "heatmap", 0.006),
// case("symbol", "binned_scatter_diamonds", 0.001),
// case("symbol", "binned_scatter_square", 0.001),
// case("symbol", "binned_scatter_triangle-down", 0.001),
// case("symbol", "binned_scatter_triangle-up", 0.001),
// case("symbol", "binned_scatter_triangle-left", 0.001),
// case("symbol", "binned_scatter_triangle-right", 0.001),
// case("symbol", "binned_scatter_triangle", 0.001),
// case("symbol", "binned_scatter_wedge", 0.001),
// case("symbol", "binned_scatter_arrow", 0.001),
// case("symbol", "binned_scatter_cross", 0.001),
// case("symbol", "binned_scatter_circle", 0.001),
// case("symbol", "binned_scatter_path", 0.001),
// case("symbol", "binned_scatter_path_star", 0.001),
// case("symbol", "binned_scatter_cross_stroke", 0.001),
// case("symbol", "binned_scatter_circle_stroke", 0.001),
// case("symbol", "binned_scatter_circle_stroke_no_fill", 0.001),
// case("symbol", "binned_scatter_path_star_stroke_no_fill", 0.001),
// case("symbol", "scatter_transparent_stroke", 0.001),
// case("symbol", "scatter_transparent_stroke_star", 0.006),
// case("symbol", "wind_vector", 0.0015),
// case("symbol", "wedge_angle", 0.001),
// case("symbol", "wedge_stroke_angle", 0.001),
// case("symbol", "zindex_circles", 0.001),
// case("symbol", "mixed_symbols", 0.001),
// case("rule", "wide_rule_axes", 0.0001),
case("rule", "wide_transparent_butt", 0.0001),
// case("text", "bar_axis_labels", 0.025)
case("rect", "stacked_bar", 0.001),
case("rect", "heatmap", 0.006),
case("symbol", "binned_scatter_diamonds", 0.001),
case("symbol", "binned_scatter_square", 0.001),
case("symbol", "binned_scatter_triangle-down", 0.001),
case("symbol", "binned_scatter_triangle-up", 0.001),
case("symbol", "binned_scatter_triangle-left", 0.001),
case("symbol", "binned_scatter_triangle-right", 0.001),
case("symbol", "binned_scatter_triangle", 0.001),
case("symbol", "binned_scatter_wedge", 0.001),
case("symbol", "binned_scatter_arrow", 0.001),
case("symbol", "binned_scatter_cross", 0.001),
case("symbol", "binned_scatter_circle", 0.001),
case("symbol", "binned_scatter_path", 0.001),
case("symbol", "binned_scatter_path_star", 0.001),
case("symbol", "binned_scatter_cross_stroke", 0.001),
case("symbol", "binned_scatter_circle_stroke", 0.001),
case("symbol", "binned_scatter_circle_stroke_no_fill", 0.001),
case("symbol", "binned_scatter_path_star_stroke_no_fill", 0.001),
case("symbol", "scatter_transparent_stroke", 0.001),
case("symbol", "scatter_transparent_stroke_star", 0.006),
case("symbol", "wind_vector", 0.0015),
case("symbol", "wedge_angle", 0.001),
case("symbol", "wedge_stroke_angle", 0.001),
case("symbol", "zindex_circles", 0.001),
case("symbol", "mixed_symbols", 0.001),
case("rule", "wide_rule_axes", 0.0001),
case("rule", "wide_transparent_caps", 0.0001),
case("text", "bar_axis_labels", 0.025)
)]
fn test_image_baseline(category: &str, spec_name: &str, tolerance: f64) {
let specs_dir = format!(
Expand Down

0 comments on commit d407dad

Please sign in to comment.