fugafacere
Version:
A pure-JS implementation of the W3C's Canvas-2D Context API that can run on top of either Expo Graphics or a browser WebGL context.
638 lines (520 loc) • 16.8 kB
JavaScript
import stringFormat from 'string-format';
const shaderVersion = `#version 300 es
`;
const shaderCommonHeaderTxt = `
#define M_PI 3.1415926535897932384626433832795
precision lowp int;
precision mediump float;
precision mediump sampler2DArray;
`;
const shaderFragHeaderTxt = `
out vec4 fragColor;
uniform float uGlobalAlpha;
void fragShaderPostprocess(void) {
fragColor *= vec4(1, 1, 1, uGlobalAlpha);
fragColor.a *= runTextFragShader();
}
`;
const textVertShaderTxt = `
in vec3 aTextPageCoord;
uniform bool uTextEnabled;
out vec3 vTextPageCoord;
void runTextVertShader(void) {
if (uTextEnabled) {
vTextPageCoord = aTextPageCoord;
}
}
`;
const textFragShaderTxt = `
uniform sampler2DArray uTextPages;
uniform bool uTextEnabled;
uniform float uTextDistanceFieldThreshold;
uniform float uTextStrokeWidth;
in vec3 vTextPageCoord;
float runTextFragShader() {
if (uTextEnabled) {
float textMask = texture(uTextPages, vTextPageCoord).a;
if (uTextStrokeWidth < 0.0) {
if (textMask < uTextDistanceFieldThreshold) {
textMask = 0.0;
} else {
textMask = 1.0;
}
} else {
float lowerThreshold = max(0.0001, uTextDistanceFieldThreshold - uTextStrokeWidth);
float upperThreshold = min(0.9999, uTextDistanceFieldThreshold + uTextStrokeWidth);
if (textMask < lowerThreshold || textMask > upperThreshold) {
textMask = 0.0;
} else {
textMask = 1.0;
}
}
return textMask;
} else {
return 1.0;
}
}
`;
export const flatShaderTxt = {
vert:
shaderVersion +
shaderCommonHeaderTxt +
textVertShaderTxt +
`
in vec2 aVertexPosition;
uniform bool uSkipMVTransform;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
void main(void) {
if (uSkipMVTransform) {
gl_Position = uPMatrix * vec4(aVertexPosition, 0.0, 1.0);
} else {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 0.0, 1.0);
}
runTextVertShader();
}
`,
frag:
shaderVersion +
shaderCommonHeaderTxt +
textFragShaderTxt +
shaderFragHeaderTxt +
`
uniform vec4 uColor;
void main(void) {
fragColor = uColor;
fragShaderPostprocess();
}
`,
};
// TODO: right now we're testing with 128 grad stops and it seems to not
// crash, but it's unclear how performance is. It might be a better idea to
// use a texture generated by CPU for color lookup, in which case mapToGradStop
// just becomes a sampler operation - look into this if things get untenable
const gradMapperFragShaderTxt = `
const int MAX_STOPS = {maxGradStops};
uniform vec4 colors[MAX_STOPS];
uniform float offsets[MAX_STOPS];
vec4 mapToGradStop(float t) {
vec4 stopColor = colors[0];
for(int i = 0; i < MAX_STOPS; i ++) {
if (offsets[i+1] == -1.0) {
stopColor = colors[i];
break;
}
if (t >= offsets[i] && t < offsets[i+1] ) {
float stopOffset = t-offsets[i];
stopOffset /= offsets[i+1] - offsets[i];
stopColor = mix(colors[i], colors[i+1], stopOffset);
break;
}
}
return stopColor;
}
`;
export const linearGradShaderTxt = {
vert:
shaderVersion +
shaderCommonHeaderTxt +
textVertShaderTxt +
`
in vec2 aVertexPosition;
out vec2 vP2;
uniform bool uSkipMVTransform;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform mat4 uiMVMatrix;
void main(void) {
if (uSkipMVTransform) {
gl_Position = uPMatrix * vec4(aVertexPosition, 0.0, 1.0);
vP2 = (uiMVMatrix * vec4(aVertexPosition, 0.0, 1.0)).xy;
} else {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 0.0, 1.0);
vP2 = aVertexPosition.xy;
}
runTextVertShader();
}
`,
frag:
shaderVersion +
shaderCommonHeaderTxt +
textFragShaderTxt +
shaderFragHeaderTxt +
gradMapperFragShaderTxt +
`
uniform vec2 p0;
uniform vec2 p1;
in vec2 vP2;
const float epsilon = 0.01;
void main() {
// Project coordinate onto gradient spectrum
vec2 p1p0 = p1 - p0;
vec2 p2p0 = vP2 - p0;
float t = dot(p2p0, p1p0) / dot(p1p0, p1p0);
t = clamp(t, 0.0, 1.0);
// Map to color
fragColor = mapToGradStop(t);
fragShaderPostprocess();
}
`,
};
export const radialGradShaderTxt = {
vert:
shaderVersion +
shaderCommonHeaderTxt +
textVertShaderTxt +
`
in vec2 aVertexPosition;
out vec2 vP2;
uniform bool uSkipMVTransform;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform mat4 uiMVMatrix;
void main(void) {
if (uSkipMVTransform) {
gl_Position = uPMatrix * vec4(aVertexPosition, 0.0, 1.0);
vP2 = (uiMVMatrix * vec4(aVertexPosition, 0.0, 1.0)).xy;
} else {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 0.0, 1.0);
vP2 = aVertexPosition.xy;
}
runTextVertShader();
}
`,
frag:
shaderVersion +
shaderCommonHeaderTxt +
textFragShaderTxt +
shaderFragHeaderTxt +
gradMapperFragShaderTxt +
`
uniform vec2 p0;
uniform float r0;
uniform vec2 p1;
uniform float r1;
uniform bool uCirclesTouching;
in vec2 vP2;
void main() {
if (uCirclesTouching) {
vec2 gradLine = normalize(p1-p0);
vec2 touchPt = p0 - gradLine*r0;
if (dot(vP2-touchPt, gradLine) < 0.0) {
fragColor = vec4(1,1,1,0);
fragShaderPostprocess();
return;
}
}
// Project coordinate onto gradient spectrum
float t;
if (distance(vP2, p0) < r0) {
t = 0.0;
} else if (distance(vP2, p1) > r1) {
t = 1.0;
} else {
vec2 p2p0 = vP2 - p0;
float c0theta = atan(p2p0.y, p2p0.x);
vec2 radialP0 = vec2(r0*cos(c0theta), r0*sin(c0theta)) + p0;
//vec2 radialP1 = vec2(r1*cos(c0theta), r1*sin(c0theta)) + p0;
vec2 e = normalize(radialP0 - vP2);
vec2 h = p1 - radialP0;
float lf = dot(e,h);
float s = r1*r1-dot(h,h)+lf*lf;
// TODO: if s < 0, no intersection pts, what to do?
s = sqrt(s);
vec2 radialP1;
if (lf < s) {
if (lf + s >= 0.0) {
//s = -s;
// TODO: tangent pt. wtf.
}
// TODO: else no intersection? wtf?
} else {
radialP1 = e*(lf-s) + radialP0;
}
vec2 rp1p0 = radialP1 - radialP0;
vec2 rp2p0 = vP2 - radialP0;
t = dot(rp2p0, rp1p0) / dot(rp1p0, rp1p0);
}
t = clamp(t, 0.0, 1.0);
// Map to color
fragColor = mapToGradStop(t);
fragShaderPostprocess();
}
`,
};
export const disjointRadialGradShaderTxt = {
vert:
shaderVersion +
shaderCommonHeaderTxt +
textVertShaderTxt +
`
in vec2 aVertexPosition;
out vec2 vP2;
uniform bool uSkipMVTransform;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform mat4 uiMVMatrix;
void main(void) {
if (uSkipMVTransform) {
gl_Position = uPMatrix * vec4(aVertexPosition, 0.0, 1.0);
vP2 = (uiMVMatrix * vec4(aVertexPosition, 0.0, 1.0)).xy;
} else {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 0.0, 1.0);
vP2 = aVertexPosition.xy;
}
runTextVertShader();
}
`,
frag:
shaderVersion +
shaderCommonHeaderTxt +
textFragShaderTxt +
shaderFragHeaderTxt +
gradMapperFragShaderTxt +
`
uniform vec2 p0;
uniform vec2 p1;
uniform float r0;
uniform float r1;
uniform vec2 uPinchPt;
in vec2 vP2;
uniform bool uStopDirection;
void circleIntersection(in vec2 testPt, in vec2 pinchPt, in vec2 circlePt, in float circleRad, in bool stopDirection, out highp vec2 intersection, out bool valid){
// TODO: this routine seems to be numerically unstable
// on some platforms when floats are mediump/lowp - quantify
// exactly where this becomes a problem
vec2 p2p0 = testPt - pinchPt;
vec2 p1p0 = circlePt - pinchPt;
float proj_t = dot(p2p0,p1p0) / pow(length(p2p0),2.0);
if (proj_t < 0.0) {
valid = false;
return;
}
highp vec2 p2p1 = testPt - circlePt;
highp vec2 p0p1 = pinchPt - circlePt;
highp float dx = p2p1.x - p0p1.x;
highp float dy = p2p1.y - p0p1.y; // ABSing this fixes bad solution detection? is that the right thing to do?
highp float dr = sqrt(dx*dx + dy*dy);
highp float D = (p0p1.x*p2p1.y) - (p2p1.x*p0p1.y);
highp float discriminant = pow(circleRad*dr,2.0)-pow(D,2.0);
if (dr == 0.0) {
valid = false;
return;
}
if (discriminant <= 0.0) {
valid = false;
return;
}
float dysgn = (dy < 0.0) ? -1.0 : 1.0;
float stopDirectionSgn = (stopDirection) ? dysgn:-dysgn;
intersection = vec2(
( D*dy + stopDirectionSgn*dysgn*dx*sqrt(discriminant)) / pow(dr,2.0),
(-D*dx + stopDirectionSgn*abs(dy) *sqrt(discriminant)) / pow(dr,2.0)
);
intersection += circlePt;
valid = true;
return;
}
void main() {
// Project coordinate onto gradient spectrum
// TODO: describe the geometry here
float t = 0.0;
vec2 pinchPt;
bool stopDirection;
bool skipGradCalc = false;
// TODO: decompose main into mainEqualRadii() and mainUnequalRadii()
// TODO: solution selection is inconsistent with ''var g = ctx.createRadialGradient(230, 25, 50, 100, 25, 20);''
if (r0 == r1) {
vec2 gradDirection = normalize(p1-p0);
float vP2projection = dot(vP2-p0, gradDirection);
if (vP2projection < -r0) {
t = 0.0;
skipGradCalc = true;
vec2 vP2projectionPt = p0 + vP2projection*gradDirection;
if (length(vP2projectionPt-vP2) > r0) {
fragColor = vec4(1,1,1,0);
fragShaderPostprocess();
return;
}
} else if (vP2projection > length(p1-p0)+r0) {
t = 1.0;
vec2 vP2projectionPt = p0 + vP2projection*gradDirection;
if (length(vP2projectionPt-vP2) > r0) {
fragColor = vec4(1,1,1,0);
fragShaderPostprocess();
return;
}
skipGradCalc = true;
} else {
pinchPt = vP2 - gradDirection*max(length(vP2-p0),r0)*2.0;
}
stopDirection = !uStopDirection;
} else {
pinchPt = uPinchPt;
stopDirection = uStopDirection;
}
if (!skipGradCalc) {
highp vec2 intersection0;
highp vec2 intersection1;
bool valid0;
bool valid1;
circleIntersection(
vP2, // test point
pinchPt, // "pinch" point
p0, r0, // circle center/radius
stopDirection, // solution selection
intersection0, valid0 // outputs
);
circleIntersection(
vP2, // test point
pinchPt, // "pinch" point
p1, r1, // circle center/radius
stopDirection, // solution selection
intersection1, valid1 // outputs
);
if (!valid0 || !valid1) {
fragColor = vec4(1,1,1,0);
fragShaderPostprocess();
return;
}
vec2 gradLine = normalize(vP2 - pinchPt);
float minPt = dot(intersection0-pinchPt, gradLine);
float maxPt = dot(intersection1-pinchPt, gradLine) - minPt;
float fragPt = dot(vP2-pinchPt, gradLine) - minPt;
fragPt = clamp(fragPt, 0.0, maxPt);
t = fragPt / maxPt;
}
// Map to color
fragColor = mapToGradStop(t);
fragShaderPostprocess();
}
`,
};
export const patternShaderRepeatValues = {
'no-repeat': 0,
'repeat-x': 1,
'repeat-y': 2,
repeat: 3,
'src-rect': 4, // Only used for drawImage() and putImageData()
};
export const patternShaderTxt = {
vert:
shaderVersion +
shaderCommonHeaderTxt +
textVertShaderTxt +
stringFormat(
`
in vec2 aVertexPosition;
in vec2 aTexCoord;
uniform bool uSkipMVTransform;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform int uRepeatMode;
uniform vec2 uTextureSize;
out vec2 vTexCoord;
uniform mat4 uiMVMatrix;
void main(void) {
if (uSkipMVTransform) {
gl_Position = uPMatrix * vec4(aVertexPosition, 0.0, 1.0);
vTexCoord = (uiMVMatrix * vec4(aVertexPosition, 0.0, 1.0)).xy / uTextureSize;
} else {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 0.0, 1.0);
vTexCoord = aVertexPosition / uTextureSize;
}
if (uRepeatMode == {src-rect}) {
vTexCoord = aTexCoord;
}
runTextVertShader();
}
`,
patternShaderRepeatValues
),
frag:
shaderVersion +
shaderCommonHeaderTxt +
textFragShaderTxt +
shaderFragHeaderTxt +
stringFormat(
`
uniform int uRepeatMode;
uniform sampler2D uTexture;
in vec2 vTexCoord;
void main(void) {
if ((uRepeatMode == {no-repeat} || uRepeatMode == {src-rect}) && (
vTexCoord.x < 0.0 || vTexCoord.x > 1.0 ||
vTexCoord.y < 0.0 || vTexCoord.y > 1.0))
{
fragColor = vec4(0,0,0,0);
} else if (uRepeatMode == {repeat-x} && (
vTexCoord.y < 0.0 || vTexCoord.y > 1.0))
{
fragColor = vec4(0,0,0,0);
} else if (uRepeatMode == {repeat-y} && (
vTexCoord.x < 0.0 || vTexCoord.x > 1.0))
{
fragColor = vec4(0,0,0,0);
} else {
vec2 wrappedCoord = mod(vTexCoord, 1.0);
fragColor = texture(uTexture, wrappedCoord).rgba;
}
fragShaderPostprocess();
}
`,
patternShaderRepeatValues
),
};
export class ShaderProgram {
constructor(gl, vertShaderTxt, fragShaderTxt) {
this.gl = gl;
const vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, vertShaderTxt);
gl.compileShader(vertShader);
if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
console.log(gl.getShaderInfoLog(vertShader));
throw new SyntaxError('Error compiling vertex shader: \n' + gl.getShaderInfoLog(vertShader));
}
const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, fragShaderTxt);
gl.compileShader(fragShader);
if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
console.log(gl.getShaderInfoLog(fragShader));
throw new SyntaxError(
'Error compiling fragment shader: \n' + gl.getShaderInfoLog(fragShader)
);
}
this.programHandle = gl.createProgram();
gl.attachShader(this.programHandle, vertShader);
gl.attachShader(this.programHandle, fragShader);
gl.linkProgram(this.programHandle);
if (!gl.getProgramParameter(this.programHandle, gl.LINK_STATUS)) {
console.log(gl.getProgramInfoLog(this.programHandle));
throw new SyntaxError(
'Error linking shader program: \n' + gl.getProgramInfoLog(this.programHandle)
);
}
gl.useProgram(this.programHandle);
this.vertexArray = gl.createVertexArray();
gl.bindVertexArray(this.vertexArray);
this.attributes = {};
const nAttributes = gl.getProgramParameter(this.programHandle, gl.ACTIVE_ATTRIBUTES);
let names = [];
for (let i = 0; i < nAttributes; i++) {
const attr = gl.getActiveAttrib(this.programHandle, i);
names.push(attr.name);
this.attributes[attr.name] = gl.getAttribLocation(this.programHandle, attr.name);
gl.enableVertexAttribArray(this.attributes[attr.name]);
}
this.uniforms = {};
const nUniforms = gl.getProgramParameter(this.programHandle, gl.ACTIVE_UNIFORMS);
names = [];
for (let i = 0; i < nUniforms; i++) {
const uniform = gl.getActiveUniform(this.programHandle, i);
names.push(uniform.name);
this.uniforms[uniform.name] = gl.getUniformLocation(this.programHandle, uniform.name);
}
}
bind() {
this.gl.useProgram(this.programHandle);
this.gl.bindVertexArray(this.vertexArray);
}
}