Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/PenSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,19 @@ class PenSkin extends Skin {
__premultipliedColor[2] = penColor[2] * penColor[3];
__premultipliedColor[3] = penColor[3];

// Fun fact: Doing this calculation in the shader has the potential to overflow the floating-point range.
// 'mediump' precision is only required to have a range up to 2^14 (16384), so any lines longer than 2^7 (128)
// can overflow that, because you're squaring the operands, and they could end up as "infinity".
// Even GLSL's `length` function won't save us here:
// https://asawicki.info/news_1596_watch_out_for_reduced_precision_normalizelength_in_opengl_es
const lineDiffX = x1 - x0;
const lineDiffY = y1 - y0;
const lineLength = Math.sqrt((lineDiffX * lineDiffX) + (lineDiffY * lineDiffY));

const uniforms = {
u_lineColor: __premultipliedColor,
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
u_lineLength: lineLength,
u_penPoints: [x0, -y0, x1, -y1],
u_stageSize: this.size
};
Expand Down
50 changes: 17 additions & 33 deletions src/shaders/sprite.frag
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ uniform float u_ghost;
#ifdef DRAW_MODE_line
uniform vec4 u_lineColor;
uniform float u_lineThickness;
uniform float u_lineLength;
uniform vec4 u_penPoints;
#endif // DRAW_MODE_line

Expand Down Expand Up @@ -217,38 +218,21 @@ void main()

#else // DRAW_MODE_line
// Maaaaagic antialiased-line-with-round-caps shader.
// Adapted from Inigo Quilez' 2D distance function cheat sheet
// https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm

// On (some?) devices with 16-bit float precision, sufficiently long lines will overflow the float's range.
// Avoid this by scaling these problematic values down to fit within (-1, 1) then scaling them back up later.
// TODO: Is this a problem on all drivers with 16-bit mediump floats, or just Mali?
vec2 pointDiff = abs(u_penPoints.zw - u_penPoints.xy);
float FLOAT_SCALING_INVERSE = max(1.0, max(pointDiff.x, pointDiff.y));
float FLOAT_SCALING = 1.0 / FLOAT_SCALING_INVERSE;

// The xy component of u_penPoints is the first point; the zw is the second point.
// This is done to minimize the number of gl.uniform calls, which can add up.
// vec2 pa = v_texCoord - u_penPoints.xy, ba = u_penPoints.zw - u_penPoints.xy;
vec2 pa = (v_texCoord - u_penPoints.xy) * FLOAT_SCALING, ba = (u_penPoints.zw - u_penPoints.xy) * FLOAT_SCALING;

// Avoid division by zero
float baDot = dot(ba, ba);
// the dot product of a vector and itself is always positive
baDot = max(baDot, epsilon);

// Magnitude of vector projection of this fragment onto the line (both relative to the line's start point).
// This results in a "linear gradient" which goes from 0.0 at the start point to 1.0 at the end point.
float projMagnitude = clamp(dot(pa, ba) / baDot, 0.0, 1.0);

float lineDistance = length(pa - (ba * projMagnitude)) * FLOAT_SCALING_INVERSE;

// The distance to the line allows us to create lines of any thickness.
// Instead of checking whether this fragment's distance < the line thickness,
// utilize the distance field to get some antialiasing. Fragments far away from the line are 0,
// fragments close to the line are 1, and fragments that are within a 1-pixel border of the line are in between.
float cappedLine = clamp((u_lineThickness + 1.0) * 0.5 - lineDistance, 0.0, 1.0);

gl_FragColor = u_lineColor * cappedLine;

// "along-the-lineness". This increases parallel to the line.
// It goes from negative before the start point, to 0.5 through the start to the end, then ramps up again
// past the end point.
float d = ((v_texCoord.x - clamp(v_texCoord.x, 0.0, u_lineLength)) * 0.5) + 0.5;

// Distance from (0.5, 0.5) to (d, the perpendicular coordinate). When we're in the middle of the line,
// d will be 0.5, so the distance will be 0 at points close to the line and will grow at points further from it.
// For the "caps", d will ramp down/up, giving us rounding.
// See https://www.youtube.com/watch?v=PMltMdi1Wzg for a rough outline of the technique used to round the lines.
float line = distance(vec2(0.5), vec2(d, v_texCoord.y)) * 2.0;
// Expand out the line by its thickness.
line -= ((u_lineThickness - 1.0) * 0.5);
// Because "distance to the center of the line" decreases the closer we get to the line, but we want more opacity
// the closer we are to the line, invert it.
gl_FragColor = u_lineColor * clamp(1.0 - line, 0.0, 1.0);
#endif // DRAW_MODE_line
}
16 changes: 10 additions & 6 deletions src/shaders/sprite.vert
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ precision mediump float;
#ifdef DRAW_MODE_line
uniform vec2 u_stageSize;
uniform float u_lineThickness;
uniform float u_lineLength;
uniform vec4 u_penPoints;

// Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations.
Expand Down Expand Up @@ -31,9 +32,13 @@ void main() {
vec2 position = a_position;
float expandedRadius = (u_lineThickness * 0.5) + 1.4142135623730951;

float lineLength = length(u_penPoints.zw - u_penPoints.xy);
// The X coordinate increases along the length of the line. It's 0 at the center of the origin point
// and is in pixel-space (so at n pixels along the line, its value is n).
v_texCoord.x = mix(0.0, u_lineLength + (expandedRadius * 2.0), a_position.x) - expandedRadius;
// The Y coordinate is perpendicular to the line. It's also in pixel-space.
v_texCoord.y = ((a_position.y - 0.5) * expandedRadius) + 0.5;

position.x *= lineLength + (2.0 * expandedRadius);
position.x *= u_lineLength + (2.0 * expandedRadius);
position.y *= 2.0 * expandedRadius;

// Center around first pen point
Expand All @@ -43,19 +48,18 @@ void main() {
vec2 pointDiff = u_penPoints.zw - u_penPoints.xy;
// Ensure line has a nonzero length so it's rendered properly
// As long as either component is nonzero, the line length will be nonzero
pointDiff.x = abs(pointDiff.x) < epsilon ? epsilon : pointDiff.x;
// If the line is zero-length, give it a bit of horizontal length
pointDiff.x = (abs(pointDiff.x) < epsilon && abs(pointDiff.y) < epsilon) ? epsilon : pointDiff.x;
// The `normalized` vector holds rotational values equivalent to sine/cosine
// We're applying the standard rotation matrix formula to the position to rotate the quad to the line angle
vec2 normalized = pointDiff / max(lineLength, epsilon);
vec2 normalized = pointDiff / max(u_lineLength, epsilon);
position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position;
// Translate quad
position += u_penPoints.xy;

// Apply view transform
position *= 2.0 / u_stageSize;

gl_Position = vec4(position, 0, 1);
v_texCoord = position * 0.5 * u_stageSize;
#else
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
v_texCoord = a_texCoord;
Expand Down