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; }