You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
13 KiB
416 lines
13 KiB
const sdf_vs_src = `#version 300 es |
|
in vec2 a_a; // point from |
|
in vec2 a_b; // point to |
|
|
|
in int a_stroke_id; |
|
in vec2 a_pressure; |
|
|
|
uniform vec2 u_scale; |
|
uniform vec2 u_res; |
|
uniform vec2 u_translation; |
|
uniform int u_stroke_count; |
|
uniform int u_stroke_texture_size; |
|
uniform highp usampler2D u_stroke_data; |
|
uniform float u_fixed_pixel_width; |
|
|
|
out vec4 v_line; |
|
out vec2 v_texcoord; |
|
out vec3 v_color; |
|
|
|
flat out vec2 v_thickness; |
|
|
|
void main() { |
|
vec2 screen02; |
|
float apron = 1.0; // google "futanari inflation rule 34" |
|
|
|
int stroke_data_y = a_stroke_id / u_stroke_texture_size; |
|
int stroke_data_x = a_stroke_id % u_stroke_texture_size; |
|
|
|
vec2 line_dir = normalize(a_b - a_a); |
|
vec2 up_dir = vec2(line_dir.y, -line_dir.x); |
|
vec2 pixel = vec2(2.0) / u_res; |
|
|
|
uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); |
|
float radius = float(stroke_data.w); |
|
|
|
if (u_fixed_pixel_width > 0.0) { |
|
radius = u_fixed_pixel_width / u_scale.x; |
|
} |
|
|
|
float rscale = apron / u_scale.x; |
|
int vertex_index = gl_VertexID % 6; |
|
|
|
vec2 outwards; |
|
vec2 origin; |
|
|
|
if (vertex_index == 0) { |
|
// "top left" aka "p1" |
|
origin = a_a; |
|
outwards = up_dir - line_dir; |
|
} else if (vertex_index == 1 || vertex_index == 5) { |
|
// "top right" aka "p2" |
|
origin = a_b; |
|
outwards = up_dir + line_dir; |
|
} else if (vertex_index == 2 || vertex_index == 4) { |
|
// "bottom left" aka "p3" |
|
origin = a_a; |
|
outwards = -up_dir - line_dir; |
|
} else { |
|
// "bottom right" aka "p4" |
|
origin = a_b; |
|
outwards = -up_dir + line_dir; |
|
} |
|
|
|
vec2 pos = origin + normalize(outwards) * radius * 2.0 * max(a_pressure.x, a_pressure.y); // doubling is to account for max possible pressure |
|
screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0 + outwards * pixel * apron; |
|
v_texcoord = pos.xy + outwards * rscale; |
|
screen02.y = 2.0 - screen02.y; |
|
v_line = vec4(a_a, a_b); |
|
v_thickness = radius * a_pressure; // pressure 0.5 is the "neutral" pressure |
|
v_color = vec3(stroke_data.xyz) / 255.0; |
|
if (a_stroke_id >> 31 != 0) { |
|
screen02 += vec2(100.0); // shift offscreen |
|
} |
|
gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); |
|
} |
|
`; |
|
|
|
const sdf_fs_src = `#version 300 es |
|
precision highp float; |
|
|
|
uniform int u_debug_mode; |
|
|
|
in vec4 v_line; |
|
in vec2 v_texcoord; |
|
in vec3 v_color; |
|
|
|
flat in vec2 v_thickness; |
|
|
|
layout(location = 0) out vec4 FragColor; |
|
|
|
void main() { |
|
if (u_debug_mode == 0) { |
|
vec2 a = v_line.xy; |
|
vec2 b = v_line.zw; |
|
|
|
vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy; |
|
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); |
|
float dist = length(v_texcoord - (a + ba * h)) - mix(v_thickness.x, v_thickness.y, h); |
|
|
|
float fade = 0.5 * length(fwidth(v_texcoord)); |
|
float alpha = 1.0 - smoothstep(-fade, fade, dist); |
|
|
|
//if (alpha > 0.5) alpha = 0.5; |
|
|
|
FragColor = vec4(v_color * alpha, alpha); |
|
} else { |
|
FragColor = vec4(0.2, 0.0, 0.0, 0.2); |
|
} |
|
} |
|
`; |
|
|
|
const tquad_vs_src = `#version 300 es |
|
in vec2 a_pos; |
|
|
|
uniform vec2 u_scale; |
|
uniform vec2 u_res; |
|
uniform vec2 u_translation; |
|
|
|
out vec2 v_texcoord; |
|
|
|
void main() { |
|
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
|
vec2 screen02 = screen01 * 2.0; |
|
|
|
int vertex_index = gl_VertexID % 6; |
|
|
|
if (vertex_index == 0) { |
|
v_texcoord = vec2(0.0, 0.0); |
|
} else if (vertex_index == 1 || vertex_index == 5) { |
|
v_texcoord = vec2(1.0, 0.0); |
|
} else if (vertex_index == 2 || vertex_index == 4) { |
|
v_texcoord = vec2(0.0, 1.0); |
|
} else { |
|
v_texcoord = vec2(1.0, 1.0); |
|
} |
|
|
|
screen02.y = 2.0 - screen02.y; |
|
vec2 screen11 = screen02 - 1.0; |
|
|
|
gl_Position = vec4(screen11, 0, 1); |
|
} |
|
`; |
|
|
|
const tquad_fs_src = `#version 300 es |
|
precision highp float; |
|
|
|
in vec2 v_texcoord; |
|
|
|
uniform sampler2D u_texture; |
|
uniform int u_solid; |
|
uniform vec4 u_color; |
|
|
|
layout(location = 0) out vec4 FragColor; |
|
|
|
void main() { |
|
if (u_solid == 0) { |
|
FragColor = texture(u_texture, v_texcoord); |
|
} else { |
|
FragColor = u_color; |
|
} |
|
} |
|
`; |
|
|
|
const grid_vs_src = `#version 300 es |
|
in vec2 a_data; // per-instance |
|
|
|
out float v_fadeout; |
|
|
|
uniform vec2 u_scale; |
|
uniform vec2 u_res; |
|
uniform vec2 u_translation; |
|
uniform float u_fadeout; |
|
|
|
void main() { |
|
vec2 origin; |
|
vec2 minor_offset; |
|
vec2 major_offset; |
|
|
|
vec2 pixel = 2.0 / u_res; |
|
|
|
if (a_data.x > 0.0) { |
|
// Vertical, treat Y as X |
|
float x = a_data.y; |
|
origin = vec2(x, 0.0); |
|
minor_offset = pixel * vec2(1.0, 0.0); |
|
major_offset = vec2(0.0, 2.0); |
|
} else { |
|
// Horizontal, treat Y as Y |
|
float y = a_data.y; |
|
origin = vec2(0.0, y); |
|
minor_offset = pixel * vec2(0.0, 1.0); |
|
major_offset = vec2(2.0, 0.0); |
|
} |
|
|
|
vec2 v = (origin * u_scale + u_translation) / u_res * 2.0; |
|
vec2 pos; |
|
|
|
if (a_data.x > 0.0) { |
|
v.y = 0.0; |
|
} else { |
|
v.x = 0.0; |
|
} |
|
|
|
if (gl_VertexID % 6 == 0) { |
|
pos = v; |
|
} else if (gl_VertexID % 6 == 1 || gl_VertexID % 6 == 5) { |
|
pos = v + (a_data.x > 0.0 ? minor_offset : major_offset); |
|
//pos = v + minor_offset; |
|
} else if (gl_VertexID % 6 == 2 || gl_VertexID % 6 == 4) { |
|
pos = v + (a_data.x > 0.0 ? major_offset : minor_offset); |
|
//pos = v + major_offset; |
|
} else if (gl_VertexID % 6 == 3) { |
|
pos = v + major_offset + minor_offset; |
|
//pos = v + major_offset + minor_offset; |
|
} |
|
|
|
vec2 screen02 = pos; |
|
screen02.y = 2.0 - screen02.y; |
|
v_fadeout = u_fadeout; |
|
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); |
|
} |
|
`; |
|
|
|
const dots_vs_src = `#version 300 es |
|
in vec2 a_center; // per-instance |
|
|
|
out float v_fadeout; |
|
|
|
uniform vec2 u_scale; |
|
uniform vec2 u_res; |
|
uniform vec2 u_translation; |
|
uniform float u_fadeout; |
|
|
|
void main() { |
|
vec2 v = (a_center * u_scale + u_translation) / u_res * 2.0; |
|
vec2 pos; |
|
vec2 pixel = 2.0 / u_res; |
|
|
|
if (gl_VertexID % 6 == 0) { |
|
pos = v + pixel * vec2(-1.0); |
|
} else if (gl_VertexID % 6 == 1) { |
|
pos = v + pixel * vec2(1.0, -1.0); |
|
} else if (gl_VertexID % 6 == 2) { |
|
pos = v + pixel * vec2(-1.0, 1.0); |
|
} else if (gl_VertexID % 6 == 3) { |
|
pos = v + pixel * vec2(1.0); |
|
} else if (gl_VertexID % 6 == 4) { |
|
pos = v + pixel * vec2(-1.0, 1.0); |
|
} else if (gl_VertexID % 6 == 5) { |
|
pos = v + pixel * vec2(1.0, -1.0); |
|
} |
|
|
|
vec2 screen02 = pos; |
|
screen02.y = 2.0 - screen02.y; |
|
v_fadeout = u_fadeout; |
|
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); |
|
} |
|
`; |
|
|
|
const dots_fs_src = `#version 300 es |
|
precision highp float; |
|
|
|
in float v_fadeout; |
|
|
|
layout(location = 0) out vec4 FragColor; |
|
|
|
void main() { |
|
vec3 color = vec3(0.5); |
|
FragColor = vec4(color * v_fadeout, v_fadeout); |
|
} |
|
`; |
|
|
|
function init_webgl(state, context) { |
|
context.canvas = document.querySelector('#c'); |
|
context.gl = context.canvas.getContext('webgl2', { |
|
'preserveDrawingBuffer': true, |
|
'desynchronized': true, |
|
'antialias': false, |
|
}); |
|
|
|
const gl = context.gl; |
|
|
|
gl.enable(gl.BLEND); |
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); |
|
//gl.blendEquation(gl.MAX); |
|
|
|
//gl.enable(gl.DEPTH_TEST); |
|
//gl.depthFunc(gl.GEQUAL); |
|
|
|
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); |
|
if (context.gpu_timer_ext === null) { |
|
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query'); |
|
} |
|
|
|
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); |
|
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); |
|
|
|
const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src); |
|
const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src); |
|
|
|
const dots_vs = create_shader(gl, gl.VERTEX_SHADER, dots_vs_src); |
|
const dots_fs = create_shader(gl, gl.FRAGMENT_SHADER, dots_fs_src); |
|
|
|
const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_src); |
|
|
|
context.programs = { |
|
'image': create_program(gl, quad_vs, quad_fs), |
|
'main': create_program(gl, sdf_vs, sdf_fs), |
|
'dots': create_program(gl, dots_vs, dots_fs), |
|
'grid': create_program(gl, grid_vs, dots_fs), |
|
}; |
|
|
|
context.buffers = { |
|
'b_images': gl.createBuffer(), |
|
'b_strokes_static': gl.createBuffer(), |
|
'b_strokes_dynamic': gl.createBuffer(), |
|
'b_instance_dot': gl.createBuffer(), |
|
'b_instance_grid': gl.createBuffer(), |
|
'b_dot': gl.createBuffer(), |
|
'b_hud': gl.createBuffer(), |
|
}; |
|
|
|
context.textures = { |
|
'stroke_data': gl.createTexture(), |
|
'dynamic_stroke_data': gl.createTexture(), |
|
'ui': gl.createTexture(), |
|
}; |
|
|
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.stroke_texture_size * config.stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload |
|
|
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.dynamic_stroke_texture_size, config.dynamic_stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.dynamic_stroke_texture_size * config.dynamic_stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload |
|
|
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); |
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.ui_texture_size, config.ui_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.ui_texture_size * config.ui_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload |
|
|
|
const resize_canvas = (entries) => { |
|
// https://www.khronos.org/webgl/wiki/HandlingHighDPI |
|
const entry = entries[0]; |
|
|
|
let width; |
|
let height; |
|
|
|
if (entry.devicePixelContentBoxSize) { |
|
width = entry.devicePixelContentBoxSize[0].inlineSize; |
|
height = entry.devicePixelContentBoxSize[0].blockSize; |
|
} else if (entry.contentBoxSize) { |
|
// fallback for Safari that will not always be correct |
|
width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); |
|
height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); |
|
} |
|
|
|
context.canvas.width = width; |
|
context.canvas.height = height; |
|
|
|
schedule_draw(state, context); |
|
} |
|
|
|
const resize_observer = new ResizeObserver(resize_canvas); |
|
resize_observer.observe(context.canvas); |
|
} |
|
|
|
function create_shader(gl, type, source) { |
|
const shader = gl.createShader(type); |
|
|
|
gl.shaderSource(shader, source); |
|
gl.compileShader(shader); |
|
|
|
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { |
|
return shader; |
|
} |
|
|
|
console.error(type, ':', gl.getShaderInfoLog(shader)); |
|
|
|
gl.deleteShader(shader); |
|
} |
|
|
|
function create_program(gl, vs, fs) { |
|
const program = gl.createProgram(); |
|
|
|
gl.attachShader(program, vs); |
|
gl.attachShader(program, fs); |
|
gl.linkProgram(program); |
|
|
|
if (gl.getProgramParameter(program, gl.LINK_STATUS)) { |
|
// src: tiny-sdf |
|
// https://github.com/mapbox/tiny-sdf |
|
const wrapper = {program}; |
|
const num_attrs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); |
|
const num_uniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); |
|
|
|
wrapper.locations = {}; |
|
|
|
for (let i = 0; i < num_attrs; i++) { |
|
const attribute = gl.getActiveAttrib(program, i); |
|
wrapper.locations[attribute.name] = gl.getAttribLocation(program, attribute.name); |
|
} |
|
|
|
for (let i = 0; i < num_uniforms; i++) { |
|
const uniform = gl.getActiveUniform(program, i); |
|
wrapper.locations[uniform.name] = gl.getUniformLocation(program, uniform.name); |
|
} |
|
|
|
return wrapper; |
|
} |
|
console.error('link:', gl.getProgramInfoLog(program)); |
|
|
|
gl.deleteProgram(program); |
|
}
|
|
|