struct ChartUniform {
size: vec2<f32>,
scale: f32,
_pad: f32,
};
@group(0) @binding(0)
var<uniform> chart_uniforms: ChartUniform;
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) top_left: vec2<f32>,
@location(3) bottom_right: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) top_left: vec2<f32>,
@location(2) bottom_right: vec2<f32>,
}
// Vertex shader
@vertex
fn vs_main(
model: VertexInput
) -> VertexOutput {
var out: VertexOutput;
// Compute absolute position
let position = model.position;
// Compute vertex coordinates
let x = 2.0 * position[0] / chart_uniforms.size[0] - 1.0;
let y = 2.0 * (chart_uniforms.size[1] - position[1]) / chart_uniforms.size[1] - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.color = model.color;
out.top_left = model.top_left * chart_uniforms.scale;
out.bottom_right = model.bottom_right * chart_uniforms.scale;
return out;
}
// Fragment shader
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return lookup_color(in.color, in.clip_position, in.top_left, in.bottom_right);
}
// Gradient texture binding
@group(1) @binding(0)
var gradient_texture: texture_2d<f32>;
@group(1) @binding(1)
var gradient_sampler: sampler;
// Image texture binding
@group(2) @binding(0)
var image_texture: texture_2d<f32>;
@group(2) @binding(1)
var image_sampler: sampler;
// Text texture binding
@group(3) @binding(0)
var text_texture: texture_2d<f32>;
@group(3) @binding(1)
var text_sampler: sampler;
const GRADIENT_TEXTURE_CODE = -1.0;
const IMAGE_TEXTURE_CODE = -2.0;
const TEXT_TEXTURE_CODE = -3.0;
const GRADIENT_LINEAR = 0.0;
const GRADIENT_RADIAL = 1.0;
const COLORWAY_LENGTH = 250.0;
const GRADIENT_TEXTURE_WIDTH = 256.0;
const GRADIENT_TEXTURE_HEIGHT = 256.0;
// Compute final color, potentially computing gradient
fn lookup_color(color: vec4<f32>, clip_position: vec4<f32>, top_left: vec2<f32>, bottom_right: vec2<f32>) -> vec4<f32> {
// Use textureSampleGrad instead of textureSample to avoid Uniform Control Flow error in WebGPU
// https://github.com/gpuweb/gpuweb/discussions/2899
let texXy = vec2<f32>(clip_position[0], clip_position[1]);
let dx = dpdx(texXy);
let dy = dpdx(texXy);
if (color[0] == GRADIENT_TEXTURE_CODE) {
// If the first color coordinate is a negative value, this indicates that we are computing a color from a texture
// For gradient texture, the second color component stores the gradient texture y-coordinate
let tex_coord_y = color[1];
// Extract gradient type from fist pixel
let control0 = textureSampleGrad(gradient_texture, gradient_sampler, vec2<f32>(0.0, tex_coord_y), dx, dy);
let gradient_type = control0[0];
// Extract x/y control points from second pixel
let control1 = textureSampleGrad(gradient_texture, gradient_sampler, vec2<f32>(1.0 / GRADIENT_TEXTURE_WIDTH, tex_coord_y), dx, dy);
let x0 = control1[0];
let y0 = control1[1];
let x1 = control1[2];
let y1 = control1[3];
let p0 = vec2<f32>(x0, y0);
let p1 = vec2<f32>(x1, y1);
let frag_xy = vec2<f32>(clip_position[0], clip_position[1]);
let width_height = vec2<f32>(bottom_right[0] - top_left[0], bottom_right[1] - top_left[1]);
if (gradient_type == GRADIENT_LINEAR) {
// Convert fragment coordinate into coordinate normalized to rect bounding box
let norm_xy = (frag_xy - top_left) / width_height;
let control_dist = distance(p0, p1);
let projected_dist = dot(norm_xy - p0, p1 - p0) / control_dist;
let tex_coord_x = compute_tex_x_coord(projected_dist / control_dist);
let tex_coords = vec2<f32>(tex_coord_x, tex_coord_y);
return textureSampleGrad(gradient_texture, gradient_sampler, tex_coords, dx, dy);
} else {
// Extract additional radius gradient control points from third pixel
let control2 = textureSampleGrad(gradient_texture, gradient_sampler, vec2<f32>(2.0 / GRADIENT_TEXTURE_WIDTH, tex_coord_y), dx, dy);
let r0 = control2[0];
let r1 = control2[1];
// Expand top_left and bottom_right so they form a square
var square_top_left: vec2<f32>;
var square_bottom_right: vec2<f32>;
var side: f32;
if (width_height[0] > width_height[1]) {
// wider than tall, push out y coordinates until square
let delta = (width_height[0] - width_height[1]) / 2.0;
square_top_left = vec2<f32>(top_left[0], top_left[1] - delta);
square_bottom_right = vec2<f32>(bottom_right[0], bottom_right[1] + delta);
side = width_height[0];
} else if (width_height[0] < width_height[1]) {
// taller than wide, push out x coordinates until square
let delta = (width_height[1] - width_height[0]) / 2.0;
square_top_left = vec2<f32>(top_left[0] - delta, top_left[1]);
square_bottom_right = vec2<f32>(bottom_right[0] + delta, bottom_right[1]);
side = width_height[1];
} else {
// already square
square_top_left = top_left;
square_bottom_right = bottom_right;
side = width_height[0];
}
// Normalize the fragment coordinates to square
let norm_xy = (frag_xy - square_top_left) / side;
let r_delta = r1 - r0;
var frag_radius: f32;
if (p0[0] == p1[0] && p0[1] == p1[1]) {
// Concentric circles, compute radius to p0
frag_radius = distance(norm_xy, p0);
} else {
// Offset circles,
// In this case the radius we're computing is not to p0, but to a point between
// p0 and p1.
//
// Define the following variables:
// t: Free variable such that as t scales from 0 to 1, the radius center point
// scales from p0 to p1 while the radius scales from 0 to r.
// x: Component of norm_xy along line from p0 to p1
// y: Component of norm_xy perpendicular to the line from p0 to p1
// d: Distance from p0 to p1,
//
// The equation we need to solve is:
// r1 * t = sqrt((x - d*t) & 2 + y^2).
//
// The solution below was obtained using sympy
// >>> from sympy.solvers import solve
// >>> from sympy import symbols, sqrt
// >>> r1, t, d, x, y = symbols("r1,t,d,x,y")
// >>> solutions = solve(r1 * t - sqrt((x - d * t) ** 2 + y**2), t)
//
// Take the position solution, which corresponds to positive t values
// >>> print(solutions[1])
// (-d*x + sqrt(-d**2*y**2 + r1**2*x**2 + r1**2*y**2))/(-d**2 + r1**2)
//
let centers = p1 - p0;
let d = length(centers);
let relative_xy = norm_xy - p0;
let x = dot(relative_xy, centers) / d;
let y = length(relative_xy - normalize(centers) * x);
let t = (
-d * x + sqrt(-pow(d,2.0)*pow(y,2.0) + pow(r1,2.0)*pow(x,2.0) + pow(r1,2.0)*pow(y,2.0))
) / (
-pow(d,2.0) + pow(r1,2.0)
);
frag_radius = r1 * t;
}
let grad_dist = (frag_radius - r0) / r_delta;
let tex_coord_x = compute_tex_x_coord(grad_dist);
let tex_coords = vec2<f32>(tex_coord_x, tex_coord_y);
return textureSampleGrad(gradient_texture, gradient_sampler, tex_coords, dx, dy);
}
} else if (color[0] == IMAGE_TEXTURE_CODE) {
// Image texture coordinates are stored in the second and third color components
let tex_coords = vec2<f32>(color[1], color[2]);
return textureSampleGrad(image_texture, image_sampler, tex_coords, dx, dy);
} else if (color[0] == TEXT_TEXTURE_CODE) {
// Text texture coordinates are stored in the second and third color components
let tex_coords = vec2<f32>(color[1], color[2]);
return textureSampleGrad(text_texture, text_sampler, tex_coords, dx, dy);
} else {
return color;
}
}
fn compute_tex_x_coord(grad_dist: f32) -> f32 {
let col_offset = GRADIENT_TEXTURE_WIDTH - COLORWAY_LENGTH;
return clamp(grad_dist, 0.0, 1.0) * COLORWAY_LENGTH / GRADIENT_TEXTURE_WIDTH + col_offset / GRADIENT_TEXTURE_WIDTH;
}