Skip to content
Draft
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
112 changes: 112 additions & 0 deletions libs/drawing-engine/src/programs/BrushDrawingProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { BrushProgramBase } from "./base/BrushProgramBase"
import { DrawType } from "./base/PositionColorProgramBase"
import { Color } from "@libs/shared"

export { DrawType } from "./base/PositionColorProgramBase"

export interface LineInfo {
points: number[]
pressure?: number[]
}

export class BrushDrawingProgram extends BrushProgramBase {
constructor(gl: WebGLRenderingContext, pixelDensity: number) {
super(gl, pixelDensity)
}

public syncCanvasSize(): typeof this {
super.syncCanvasSize()
this.gl.uniform2f(this.getUniformLocation("canvasSize"), this.gl.canvas.width, this.gl.canvas.height)
return this
}

public draw({ points }: Readonly<LineInfo>, { drawType = this.gl.STREAM_DRAW, ...options }: BrushDrawOptions = {}) {
this.useProgram()
this.syncCanvasSize()

this.setBrushOptions(options)

// if (pressure && pressure.length !== points.length / 2) {
// console.warn("Pressure array should be the same length as the points array", {
// pressureValues: pressure.length,
// points: points.length / 2,
// })
// }

// const hasPressureData = (pressure?.filter((p) => p > 0).length ?? 0) > 1

// if (hasPressureData) {
// this.bufferAttribute("pressure", new Float32Array(pressure!), { usage: drawType, size: 1 })
// }

const canvasBounds = [-1, -1, 1, -1, -1, 1, 1, 1]

// repeat the bounds until it's the same length as the points array
const bounds = new Array(points.length).fill(0).map((_, i) => canvasBounds[i % canvasBounds.length])

this.bufferAttribute("bounds", new Float32Array(bounds), { usage: drawType, size: 2 })
this.bufferAttribute("position", new Float32Array(points), { usage: drawType, size: 2 })
this.gl.drawArrays(this.gl.TRIANGLES, 0, points.length / 2)
this.checkError()
}

private setBrushOptions({
color = Color.BLACK,
diameter = 5.0,
minDiameter = 1.0,
opacity = 255,
hardness = 1.0,
flow = 1.0,
}: Omit<BrushDrawOptions, "drawType">) {
this.gl.uniform1f(this.getUniformLocation("opacity"), opacity)
this.gl.uniform1f(this.getUniformLocation("strokeRadiusMax"), diameter / 2)
this.gl.uniform1f(this.getUniformLocation("strokeRadiusMin"), minDiameter / 2)
this.gl.uniform2fv(this.getUniformLocation("hardness"), [hardness, hardness])
this.gl.uniform2fv(this.getUniformLocation("flow"), [flow, flow])
// not sure what this should be yet
this.gl.uniform1f(this.getUniformLocation("strokeLength"), 1.0)
this.gl.uniform3fv(this.getUniformLocation("color"), color.vec3)

const isPremultipliedAlpha = this.gl.getParameter(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL)

this.gl.uniform1i(this.getUniformLocation("isPremultipliedAlpha"), isPremultipliedAlpha ? 1 : 0)

// uniform float uOpacity;
// uniform float uStrokeRadiusMax;
// uniform float uStrokeRadiusMin;
// uniform vec2 uHardness;
// uniform vec2 uFlow;
// uniform vec3 uColor;
// uniform float uStrokeLength;
// uniform bool uIsPremultipliedAlpha;
}
}

export interface BrushDrawOptions {
/**
* The draw type to use when drawing the line. Defaults to `gl.STREAM_DRAW`.
*/
drawType?: DrawType
/*
* The color to use when drawing the line. Black if not specified.
*/
color?: Color

/**
* The diameter of the line. Defaults to 5.0.
*/
diameter?: number

/**
* The minimum diameter of the line. Defaults to 1.0.
*/
minDiameter?: number

/**
* The opacity of the line. Defaults to 255.0.
*/
opacity?: number

hardness?: number
flow?: number
}
30 changes: 30 additions & 0 deletions libs/drawing-engine/src/programs/base/BrushProgramBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { WebGLProgramBuilder } from "@libs/shared"
import { BaseProgram } from "@libs/shared"
import sourceMap, { uniformNames, attributeNames } from "../shaders/sourceMap"

const VERTEX_SHADER = "brush.vertex"
const FRAGMENT_SHADER = "brush.fragment"
const UNIFORM_NAMES = { ...uniformNames[FRAGMENT_SHADER], ...uniformNames[VERTEX_SHADER] } as const
const ATTRIBUTE_NAMES = { ...attributeNames[VERTEX_SHADER] } as const

export abstract class BrushProgramBase extends BaseProgram<keyof typeof UNIFORM_NAMES, keyof typeof ATTRIBUTE_NAMES> {
constructor(gl: WebGLRenderingContext, pixelDensity: number) {
super(BrushProgramBase.createContextStatic(gl, BrushProgramBase.createProgramStatic(gl)), pixelDensity)
}

protected static createProgramStatic(gl: WebGLRenderingContext): WebGLProgram {
return WebGLProgramBuilder.createFromSourceMap(gl, sourceMap, VERTEX_SHADER, FRAGMENT_SHADER)
}

protected createProgram(gl: WebGLRenderingContext): WebGLProgram {
return BrushProgramBase.createProgramStatic(gl)
}

protected static createContextStatic(context: WebGLRenderingContext, program: WebGLProgram) {
return BaseProgram.getProgramInfo(context, program, UNIFORM_NAMES, ATTRIBUTE_NAMES)
}

protected createProgramInfo(context: WebGLRenderingContext, program: WebGLProgram) {
return BrushProgramBase.createContextStatic(context, program)
}
}
29 changes: 29 additions & 0 deletions libs/drawing-engine/src/programs/shaders/brush.fragment.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
precision mediump float;

#define M_PI 3.1415926535897932384626433832795

uniform vec3 uColor;
uniform int uIsPremultipliedAlpha;

varying vec2 vPosition;

#require "softBrush";

void main() {
float aspect = (uStrokeLength / (2.0 * uStrokeRadiusMax)) + 1.0;

float U = gl_FragCoord.x - vPosition.x;
float V = gl_FragCoord.y - vPosition.y;
float x = U * aspect;
float y = V;

float a = alpha(x, y, aspect);

vec4 color = vec4(uColor / vec3(255.0), a);

if(uIsPremultipliedAlpha == 1) {
color.rgb *= color.a;
}

gl_FragColor = color;
}
12 changes: 12 additions & 0 deletions libs/drawing-engine/src/programs/shaders/brush.vertex.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
attribute vec2 aPosition;
attribute vec2 aBounds;

#require "normalizeCoords";

varying vec2 vPosition;

void main() {
vPosition = normalizeCoords(aPosition);

gl_Position = vec4(aBounds, 0.0, 1.0);
}
89 changes: 89 additions & 0 deletions libs/drawing-engine/src/programs/shaders/inc/softBrush.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Credit: https://jcgt.org/published/0007/01/01/paper.pdf

uniform float uOpacity;
uniform float uStrokeRadiusMax;
uniform float uStrokeRadiusMin;
uniform vec2 uHardness;
uniform vec2 uFlow;
uniform float uStrokeLength;

float psi(float x) {
return (uStrokeRadiusMax * (2.0 * x - 1.0)) / uStrokeLength;
}

float flow(float x) {
// mix = lerp
return mix(uFlow[0], uFlow[1], psi(x));
}

float hardness(float x) {
return mix(uHardness[0], uHardness[1], psi(x));
}

float radius(float x) {
return mix(0.5, uStrokeRadiusMin / (2.0 * uStrokeRadiusMax), psi(x));
}

float dist(float x, float X, float Y) {
return sqrt(pow(X - x, 2.0) + pow(Y - 0.5, 2.0));
}

float theta(float x, float X, float Y) {
return dist(x, X, Y) / radius(x);
}

float phase(float x) {
return M_PI / 2.0 * (1.0 - (1.0 / (1.0 - hardness(x))));
}

float falloff(float x, float X, float Y) {
if(theta(x, X, Y) < hardness(x)) {
return 1.0;
} else {
float piTheta = M_PI * theta(x, X, Y);
float iHardness = 1.0 - hardness(x);
return pow(cos(piTheta / (2.0 * iHardness) + phase(x)), 2.0);
}
}

float alpha(float X, float Y, float aspect) {
float BE = abs(Y - 0.5);
float BD = radius(X);
float BO = (uStrokeLength * BD) / (uStrokeRadiusMax - uStrokeRadiusMin);
float lambda = pow(BD, 2.0) - ((pow(BD, 2.0) * pow(BE, 2.0)) / pow(BO, 2.0));
float epsilon = pow(4.0 * pow(BD, 4.0), 4.0) / pow(BO, 2.0);
float r1 = 0.0;
float r2 = 0.0;

if(uStrokeRadiusMin == uStrokeRadiusMax) {
r1 = 0.25;
r2 = 0.25;
} else {
// breaks down the quadratic formula into parts so I can look at it without going crazy
float a = (2.0 * lambda) + epsilon;

// I pulled the negative out of b and c to make them easier to read
// it's reintroduced in d
float b = pow(lambda, 2.0) + (epsilon * pow(BE, 2.0));
float c = a + (4.0 * b);
float d = sqrt(-4.0 * c);

r1 = (a + d) / 2.0;
r2 = (a - d) / 2.0;
}

float X1 = clamp(X - sqrt(r1 - pow(BE, 2.0)), 0.5, aspect - 0.5);
float X2 = clamp(X + sqrt(r2 - pow(BE, 2.0)), 0.5, aspect - 0.5);

float sum = 0.0;

// Approximate integral
const float dx = 0.001;

for(float i = 0.0; i < 1.0; i += dx) {
float x = mix(X1, X2, i);
sum += flow(x) * falloff(x, X, Y) * dx;
}

return min(uOpacity / 255.0, 2.0 * uStrokeRadiusMax * sum);
}
35 changes: 29 additions & 6 deletions libs/drawing-engine/src/programs/shaders/sourceMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import positionVertexSource from "./position.vertex.glsl"
import colorFragmentSource from "./color.fragment.glsl"
import textureVertexSource from "./texture.vertex.glsl"
import textureFragmentSource from "./texture.fragment.glsl"
import brushVertexSource from "./brush.vertex.glsl"
import brushFragmentSource from "./brush.fragment.glsl"
import normalizeCoords from "./inc/normalizeCoords.glsl"
import softBrush from "./inc/softBrush.glsl"

/**
* A map of shader source code.
Expand All @@ -15,26 +18,34 @@ const sourceMap = {
"color.fragment": colorFragmentSource,
"texture.vertex": textureVertexSource,
"texture.fragment": textureFragmentSource,
"brush.vertex": brushVertexSource,
"brush.fragment": brushFragmentSource,
normalizeCoords,
softBrush,
}
export default sourceMap

type NameMap = Record<keyof typeof sourceMap, Record<string, string>>

// would be neat if I could run something to generate the uniformNames and attributeNames objects from the sourceMap

const includableScripts = {
const reusableUniforms = {
normalizeCoords: {
source: normalizeCoords,
uniforms: {
canvasSize: "uCanvasSize",
},
canvasSize: "uCanvasSize",
},
softBrush: {
opacity: "uOpacity",
strokeRadiusMax: "uStrokeRadiusMax",
strokeRadiusMin: "uStrokeRadiusMin",
hardness: "uHardness",
flow: "uFlow",
strokeLength: "uStrokeLength",
},
} as const

export const uniformNames = {
"position.vertex": {
...includableScripts.normalizeCoords.uniforms,
...reusableUniforms.normalizeCoords,
},
"color.fragment": {
color: "uColor",
Expand All @@ -44,6 +55,14 @@ export const uniformNames = {
foreground: "uForegroundTexture",
background: "uBackgroundTexture",
},
"brush.vertex": {
...reusableUniforms.normalizeCoords,
},
"brush.fragment": {
color: "uColor",
isPremultipliedAlpha: "uIsPremultipliedAlpha",
...reusableUniforms.softBrush,
},
} as const satisfies Readonly<Partial<NameMap>>

export const attributeNames = {
Expand All @@ -54,4 +73,8 @@ export const attributeNames = {
position: "aPosition",
textureCoordinates: "aTexcoord",
},
"brush.vertex": {
position: "aPosition",
bounds: "aBounds",
},
} as const satisfies Readonly<Partial<NameMap>>