From bf43ef363a4ba7398362ec0c5a9e3c87053685e2 Mon Sep 17 00:00:00 2001
From: DD Liu <liudi08@gmail.com>
Date: Thu, 9 Apr 2020 12:27:19 -0400
Subject: [PATCH] Revert "Revert "Revert "Draw pen lines via fragment shader"""

---
 src/PenSkin.js          | 117 ++++++++++++++++++++++++++++++++++------
 src/ShaderManager.js    |   4 +-
 src/shaders/sprite.frag |  42 ++++++---------
 src/shaders/sprite.vert |  61 +++++----------------
 4 files changed, 130 insertions(+), 94 deletions(-)

diff --git a/src/PenSkin.js b/src/PenSkin.js
index ddeea87d..ef457bd6 100644
--- a/src/PenSkin.js
+++ b/src/PenSkin.js
@@ -44,6 +44,11 @@ const __projectionMatrix = twgl.m4.identity();
  */
 const __modelTranslationMatrix = twgl.m4.identity();
 
+/**
+ * Reused memory location for rotation matrix for building a model matrix.
+ * @type {FloatArray}
+ */
+const __modelRotationMatrix = twgl.m4.identity();
 
 /**
  * Reused memory location for scaling matrix for building a model matrix.
@@ -130,7 +135,7 @@ class PenSkin extends Skin {
         this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.default, NO_EFFECTS);
 
         /** @type {twgl.ProgramInfo} */
-        this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.line, NO_EFFECTS);
+        this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS);
 
         this._createLineGeometry();
 
@@ -216,15 +221,10 @@ class PenSkin extends Skin {
      * @param {number} y1 - the Y coordinate of the end of the line.
      */
     drawLine (penAttributes, x0, y0, x1, y1) {
-        // For compatibility with Scratch 2.0, offset pen lines of width 1 and 3 so they're pixel-aligned.
-        // See https://github.com/LLK/scratch-render/pull/314
-        const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
-        const offset = (diameter === 1 || diameter === 3) ? 0.5 : 0;
-
         this._drawLineOnBuffer(
             penAttributes,
-            x0 + offset, y0 + offset,
-            x1 + offset, y1 + offset
+            this._rotationCenter[0] + x0, this._rotationCenter[1] - y0,
+            this._rotationCenter[0] + x1, this._rotationCenter[1] - y1
         );
 
         this._silhouetteDirty = true;
@@ -234,16 +234,72 @@ class PenSkin extends Skin {
      * Create 2D geometry for drawing lines to a framebuffer.
      */
     _createLineGeometry () {
+        // Create a set of triangulated quads that break up a line into 3 parts:
+        // 2 caps and a body. The y component of these position vertices are
+        // divided to bring a value of 1 down to 0.5 to 0. The large y values
+        // are set so they will still be at least 0.5 after division. The
+        // divisor is scaled based on the length of the line and the lines
+        // width.
+        //
+        // Texture coordinates are based on a "generated" texture whose general
+        // shape is a circle. The line caps set their texture values to define
+        // there roundedness with the texture. The body has all of its texture
+        // values set to the center of the texture so it's a solid block.
         const quads = {
             a_position: {
                 numComponents: 2,
                 data: [
+                    -0.5, 1,
+                    0.5, 1,
+                    -0.5, 100000,
+
+                    -0.5, 100000,
+                    0.5, 1,
+                    0.5, 100000,
+
+                    -0.5, 1,
+                    0.5, 1,
+                    -0.5, -1,
+
+                    -0.5, -1,
+                    0.5, 1,
+                    0.5, -1,
+
+                    -0.5, -100000,
+                    0.5, -100000,
+                    -0.5, -1,
+
+                    -0.5, -1,
+                    0.5, -100000,
+                    0.5, -1
+                ]
+            },
+            a_texCoord: {
+                numComponents: 2,
+                data: [
+                    1, 0.5,
+                    0, 0.5,
+                    1, 0,
+
+                    1, 0,
+                    0, 0.5,
+                    0, 0,
+
+                    0.5, 0,
+                    0.5, 1,
+                    0.5, 0,
+
+                    0.5, 0,
+                    0.5, 1,
+                    0.5, 1,
+
                     1, 0,
                     0, 0,
-                    1, 1,
-                    1, 1,
+                    1, 0.5,
+
+                    1, 0.5,
                     0, 0,
-                    0, 1
+                    0, 0.5
                 ]
             }
         };
@@ -288,8 +344,6 @@ class PenSkin extends Skin {
 
     /**
      * Draw a line on the framebuffer.
-     * Note that the point coordinates are in the following coordinate space:
-     * +y is down, (0, 0) is the center, and the coords range from (-width / 2, -height / 2) to (height / 2, width / 2).
      * @param {PenAttributes} penAttributes - how the line should be drawn.
      * @param {number} x0 - the X coordinate of the beginning of the line.
      * @param {number} y0 - the Y coordinate of the beginning of the line.
@@ -303,6 +357,26 @@ class PenSkin extends Skin {
 
         this._renderer.enterDrawRegion(this._lineOnBufferDrawRegionId);
 
+        const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
+        const length = Math.hypot(Math.abs(x1 - x0) - 0.001, Math.abs(y1 - y0) - 0.001);
+        const avgX = (x0 + x1) / 2;
+        const avgY = (y0 + y1) / 2;
+        const theta = Math.atan2(y0 - y1, x0 - x1);
+        const alias = 1;
+
+        // The line needs a bit of aliasing to look smooth. Add a small offset
+        // and a small size boost to scaling to give a section to alias.
+        const translationVector = __modelTranslationVector;
+        translationVector[0] = avgX - (alias / 2);
+        translationVector[1] = avgY + (alias / 4);
+
+        const scalingVector = __modelScalingVector;
+        scalingVector[0] = diameter + alias;
+        scalingVector[1] = length + diameter - (alias / 2);
+
+        const radius = diameter / 2;
+        const yScalar = (0.50001 - (radius / (length + diameter)));
+
         // Premultiply pen color by pen transparency
         const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
         __premultipliedColor[0] = penColor[0] * penColor[3];
@@ -311,10 +385,19 @@ class PenSkin extends Skin {
         __premultipliedColor[3] = penColor[3];
 
         const uniforms = {
-            u_lineColor: __premultipliedColor,
-            u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
-            u_penPoints: [x0, -y0, x1, -y1],
-            u_stageSize: this.size
+            u_positionScalar: yScalar,
+            u_capScale: diameter,
+            u_aliasAmount: alias,
+            u_modelMatrix: twgl.m4.multiply(
+                twgl.m4.multiply(
+                    twgl.m4.translation(translationVector, __modelTranslationMatrix),
+                    twgl.m4.rotationZ(theta - (Math.PI / 2), __modelRotationMatrix),
+                    __modelMatrix
+                ),
+                twgl.m4.scaling(scalingVector, __modelScalingMatrix),
+                __modelMatrix
+            ),
+            u_lineColor: __premultipliedColor
         };
 
         twgl.setUniforms(currentShader, uniforms);
diff --git a/src/ShaderManager.js b/src/ShaderManager.js
index 8c59d3b7..3f0bc613 100644
--- a/src/ShaderManager.js
+++ b/src/ShaderManager.js
@@ -174,9 +174,9 @@ ShaderManager.DRAW_MODE = {
     colorMask: 'colorMask',
 
     /**
-     * Draw a line with caps.
+     * Sample a "texture" to draw a line with caps.
      */
-    line: 'line'
+    lineSample: 'lineSample'
 };
 
 module.exports = ShaderManager;
diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag
index a9b107a3..b271fd28 100644
--- a/src/shaders/sprite.frag
+++ b/src/shaders/sprite.frag
@@ -33,11 +33,11 @@ uniform float u_mosaic;
 uniform float u_ghost;
 #endif // ENABLE_ghost
 
-#ifdef DRAW_MODE_line
+#ifdef DRAW_MODE_lineSample
 uniform vec4 u_lineColor;
-uniform float u_lineThickness;
-uniform vec4 u_penPoints;
-#endif // DRAW_MODE_line
+uniform float u_capScale;
+uniform float u_aliasAmount;
+#endif // DRAW_MODE_lineSample
 
 uniform sampler2D u_skin;
 
@@ -109,7 +109,7 @@ const vec2 kCenter = vec2(0.5, 0.5);
 
 void main()
 {
-	#ifndef DRAW_MODE_line
+	#ifndef DRAW_MODE_lineSample
 	vec2 texcoord0 = v_texCoord;
 
 	#ifdef ENABLE_mosaic
@@ -214,27 +214,15 @@ void main()
 	// Un-premultiply alpha.
 	gl_FragColor.rgb /= gl_FragColor.a + epsilon;
 	#endif
-  
-	#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
 
-	// 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;
-	// 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) / dot(ba, ba), 0.0, 1.0);
-
-	float lineDistance = length(pa - (ba * projMagnitude));
-
-	// 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;
-	#endif // DRAW_MODE_line
+	#else // DRAW_MODE_lineSample
+	gl_FragColor = u_lineColor * clamp(
+		// Scale the capScale a little to have an aliased region.
+		(u_capScale + u_aliasAmount -
+			u_capScale * 2.0 * distance(v_texCoord, vec2(0.5, 0.5))
+		) / (u_aliasAmount + 1.0),
+		0.0,
+		1.0
+	);
+	#endif // DRAW_MODE_lineSample
 }
diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert
index c92468e1..a9bbe150 100644
--- a/src/shaders/sprite.vert
+++ b/src/shaders/sprite.vert
@@ -1,57 +1,22 @@
-precision mediump float;
-
-#ifdef DRAW_MODE_line
-uniform vec2 u_stageSize;
-uniform float u_lineThickness;
-uniform vec4 u_penPoints;
-
-// Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations.
-// Smaller values can cause problems on some mobile devices.
-const float epsilon = 1e-3;
-#endif
-
-#ifndef DRAW_MODE_line
 uniform mat4 u_projectionMatrix;
 uniform mat4 u_modelMatrix;
-attribute vec2 a_texCoord;
-#endif
 
 attribute vec2 a_position;
+attribute vec2 a_texCoord;
 
 varying vec2 v_texCoord;
 
+#ifdef DRAW_MODE_lineSample
+uniform float u_positionScalar;
+#endif
+
 void main() {
-	#ifdef DRAW_MODE_line
-	// Calculate a rotated ("tight") bounding box around the two pen points.
-	// Yes, we're doing this 6 times (once per vertex), but on actual GPU hardware,
-	// it's still faster than doing it in JS combined with the cost of uniformMatrix4fv.
-
-	// Expand line bounds by sqrt(2) / 2 each side-- this ensures that all antialiased pixels
-	// fall within the quad, even at a 45-degree diagonal
-	vec2 position = a_position;
-	float expandedRadius = (u_lineThickness * 0.5) + 1.4142135623730951;
-
-	float lineLength = length(u_penPoints.zw - u_penPoints.xy);
-
-	position.x *= lineLength + (2.0 * expandedRadius);
-	position.y *= 2.0 * expandedRadius;
-
-	// Center around first pen point
-	position -= expandedRadius;
-
-	// Rotate quad to line angle
-	vec2 normalized = (u_penPoints.zw - u_penPoints.xy + epsilon) / (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;
-	#endif
+    #ifdef DRAW_MODE_lineSample
+    vec2 position = a_position;
+    position.y = clamp(position.y * u_positionScalar, -0.5, 0.5);
+    gl_Position = u_projectionMatrix * u_modelMatrix * vec4(position, 0, 1);
+    #else
+    gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
+    #endif
+    v_texCoord = a_texCoord;
 }