A.Olokhtonov
2 years ago
7 changed files with 448 additions and 392 deletions
@ -1,407 +1,89 @@ |
|||||||
document.addEventListener('DOMContentLoaded', main); |
document.addEventListener('DOMContentLoaded', main); |
||||||
|
|
||||||
const vertex_shader_source = ` |
function draw(state, context) { |
||||||
attribute vec2 a_pos; |
const gl = context.gl; |
||||||
attribute vec3 a_triangle_color; |
const locations = context.locations; |
||||||
|
const buffers = context.buffers; |
||||||
uniform vec2 u_scale; |
|
||||||
uniform vec2 u_res; |
|
||||||
uniform vec2 u_translation; |
|
||||||
uniform int u_layer; |
|
||||||
|
|
||||||
varying vec3 v_triangle_color; |
|
||||||
|
|
||||||
void main() { |
|
||||||
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
|
||||||
vec2 screen02 = screen01 * 2.0; |
|
||||||
screen02.y = 2.0 - screen02.y; |
|
||||||
vec2 screen11 = screen02 - 1.0; |
|
||||||
|
|
||||||
v_triangle_color = a_triangle_color; |
|
||||||
|
|
||||||
gl_Position = vec4(screen11, u_layer, 1); |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
const fragment_shader_source = ` |
|
||||||
precision mediump float; |
|
||||||
|
|
||||||
uniform vec3 u_color; |
|
||||||
|
|
||||||
varying vec3 v_triangle_color; |
|
||||||
|
|
||||||
void main() { |
|
||||||
gl_FragColor = vec4(v_triangle_color, 1); |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
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)) { |
|
||||||
return program; |
|
||||||
} |
|
||||||
|
|
||||||
console.error('link:', gl.getProgramInfoLog(program)); |
|
||||||
|
|
||||||
gl.deleteProgram(program); |
|
||||||
} |
|
||||||
|
|
||||||
function perpendicular(ax, ay, bx, by, width) { |
|
||||||
// Place points at (stroke_width / 2) distance from the line
|
|
||||||
const dirx = bx - ax; |
|
||||||
const diry = by - ay; |
|
||||||
|
|
||||||
let pdirx = diry; |
|
||||||
let pdiry = -dirx; |
|
||||||
|
|
||||||
const pdir_norm = Math.sqrt(pdirx * pdirx + pdiry * pdiry); |
|
||||||
|
|
||||||
pdirx /= pdir_norm; |
|
||||||
pdiry /= pdir_norm; |
|
||||||
|
|
||||||
return { |
|
||||||
'p1': { |
|
||||||
'x': ax + pdirx * width / 2, |
|
||||||
'y': ay + pdiry * width / 2, |
|
||||||
}, |
|
||||||
|
|
||||||
'p2': { |
|
||||||
'x': ax - pdirx * width / 2, |
|
||||||
'y': ay - pdiry * width / 2, |
|
||||||
} |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
const canvas_offset = { 'x': 0, 'y': 0 }; |
|
||||||
let moving = false; |
|
||||||
let spacedown = false; |
|
||||||
let drawing = false; |
|
||||||
let canvas_zoom = 5.0; |
|
||||||
let current_stroke = []; |
|
||||||
const bgcolor = { 'r': 0, 'g': 0, 'b': 0 }; |
|
||||||
const stroke_color = { 'r': 0.2, 'g': 0.2, 'b': 0.2 }; |
|
||||||
let debug_draw = true; |
|
||||||
|
|
||||||
function push_circle_at(positions, c, o) { |
|
||||||
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[4].x, c.y + o[4].y, c.x + o[8].x, c.y + o[8].y); |
|
||||||
positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[0].x, c.y + o[0].y, c.x + o[2].x, c.y + o[2].y); |
|
||||||
positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[4].x, c.y + o[4].y, c.x + o[6].x, c.y + o[6].y); |
|
||||||
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[8].x, c.y + o[8].y, c.x + o[10].x, c.y + o[10].y); |
|
||||||
positions.push(c.x + o[2].x, c.y + o[2].y, c.x + o[0].x, c.y + o[0].y, c.x + o[1].x, c.y + o[1].y); |
|
||||||
positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[2].x, c.y + o[2].y, c.x + o[3].x, c.y + o[3].y); |
|
||||||
positions.push(c.x + o[6].x, c.y + o[6].y, c.x + o[4].x, c.y + o[4].y, c.x + o[5].x, c.y + o[5].y); |
|
||||||
positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[6].x, c.y + o[6].y, c.x + o[7].x, c.y + o[7].y); |
|
||||||
positions.push(c.x + o[10].x, c.y + o[10].y, c.x + o[8].x, c.y + o[8].y, c.x + o[9].x, c.y + o[9].y); |
|
||||||
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[10].x, c.y + o[10].y, c.x + o[11].x, c.y + o[11].y); |
|
||||||
} |
|
||||||
|
|
||||||
function push_stroke_positions(stroke, stroke_width, positions) { |
|
||||||
const points = stroke.points; |
|
||||||
|
|
||||||
if (points.length < 2) { |
|
||||||
// TODO
|
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// Simple 12 point circle (store offsets and reuse)
|
|
||||||
const POINTS = 12; |
|
||||||
const phi_step = 2 * Math.PI / POINTS; |
|
||||||
|
|
||||||
const circle_offsets = []; |
|
||||||
|
|
||||||
for (let i = 0; i < POINTS; ++i) { |
|
||||||
const phi = phi_step * i; |
|
||||||
const ox = stroke_width / 2 * Math.cos(phi); |
|
||||||
const oy = stroke_width / 2 * Math.sin(phi); |
|
||||||
circle_offsets.push({'x': ox, 'y': oy}); |
|
||||||
} |
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; ++i) { |
|
||||||
const px = points[i].x; |
|
||||||
const py = points[i].y; |
|
||||||
|
|
||||||
const nextpx = points[i + 1].x; |
|
||||||
const nextpy = points[i + 1].y; |
|
||||||
|
|
||||||
const d1x = nextpx - px; |
|
||||||
const d1y = nextpy - py; |
|
||||||
|
|
||||||
// Perpendicular to (d1x, d1y), points to the LEFT
|
|
||||||
let perp1x = -d1y; |
|
||||||
let perp1y = d1x; |
|
||||||
|
|
||||||
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); |
|
||||||
|
|
||||||
perp1x /= perpnorm1; |
|
||||||
perp1y /= perpnorm1; |
|
||||||
|
|
||||||
const s1x = px + perp1x * stroke_width / 2; |
|
||||||
const s1y = py + perp1y * stroke_width / 2; |
|
||||||
const s2x = px - perp1x * stroke_width / 2; |
|
||||||
const s2y = py - perp1y * stroke_width / 2; |
|
||||||
|
|
||||||
const s3x = nextpx + perp1x * stroke_width / 2; |
|
||||||
const s3y = nextpy + perp1y * stroke_width / 2; |
|
||||||
const s4x = nextpx - perp1x * stroke_width / 2; |
|
||||||
const s4y = nextpy - perp1y * stroke_width / 2; |
|
||||||
|
|
||||||
positions.push(s1x, s1y, s2x, s2y, s4x, s4y); |
|
||||||
positions.push(s1x, s1y, s4x, s4y, s3x, s3y); |
|
||||||
|
|
||||||
push_circle_at(positions, points[i], circle_offsets); |
|
||||||
} |
|
||||||
|
|
||||||
push_circle_at(positions, points[points.length - 1], circle_offsets); |
|
||||||
} |
|
||||||
|
|
||||||
function draw(gl, program, locations, buffers, strokes) { |
|
||||||
const width = window.innerWidth; |
const width = window.innerWidth; |
||||||
const height = window.innerHeight; |
const height = window.innerHeight; |
||||||
|
|
||||||
if (gl.canvas.width !== width || gl.canvas.height !== height) { |
gl.viewport(0, 0, context.canvas.width, context.canvas.height); |
||||||
gl.canvas.width = width; |
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); |
||||||
gl.canvas.height = height; |
|
||||||
gl.viewport(0, 0, width, height); |
|
||||||
} |
|
||||||
|
|
||||||
gl.clearColor(bgcolor.r, bgcolor.g, bgcolor.b, 1); |
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT); |
gl.clear(gl.COLOR_BUFFER_BIT); |
||||||
gl.useProgram(program); |
gl.useProgram(context.program); |
||||||
|
|
||||||
gl.enableVertexAttribArray(locations['a_pos']); |
gl.enableVertexAttribArray(locations['a_pos']); |
||||||
gl.enableVertexAttribArray(locations['a_triangle_color']); |
gl.enableVertexAttribArray(locations['a_color']); |
||||||
|
|
||||||
gl.uniform2f(locations['u_res'], width, height); |
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
gl.uniform2f(locations['u_scale'], canvas_zoom, canvas_zoom); |
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
gl.uniform2f(locations['u_translation'], canvas_offset.x, canvas_offset.y); |
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
gl.uniform1i(locations['u_layer'], 0); |
||||||
const positions = []; |
|
||||||
const colors = []; |
const total_pos_size = context.static_positions_f32.byteLength + context.dynamic_positions_f32.byteLength; |
||||||
const stroke_width = 10; |
const total_color_size = context.static_colors_u8.byteLength + context.dynamic_colors_u8.byteLength; |
||||||
|
const total_point_count = (context.static_positions.length + context.dynamic_positions.length) / 2; |
||||||
for (const stroke of strokes) { |
|
||||||
push_stroke_positions(stroke, stroke_width, positions); |
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); |
||||||
} |
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, total_pos_size, gl.DYNAMIC_DRAW); |
||||||
if (current_stroke.length > 0) { |
gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_positions_f32); |
||||||
push_stroke_positions({'points': current_stroke}, stroke_width, positions); |
gl.bufferSubData(gl.ARRAY_BUFFER, context.static_positions_f32.byteLength, context.dynamic_positions_f32); |
||||||
} |
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_color']); |
||||||
const npoints = positions.length / 2; |
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, 0, 0); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, total_color_size, gl.DYNAMIC_DRAW); |
||||||
for (let i = 0; i < npoints; i += 3) { |
gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_colors_u8); |
||||||
if (!debug_draw) { |
gl.bufferSubData(gl.ARRAY_BUFFER, context.static_colors_u8.byteLength, context.dynamic_colors_u8); |
||||||
positions.push(0, 0, 0); |
|
||||||
positions.push(0, 0, 0); |
gl.drawArrays(gl.TRIANGLES, 0, total_point_count); |
||||||
positions.push(0, 0, 0); |
|
||||||
} else { |
|
||||||
let r = (i * 761257125 % 255) / 255.0; |
|
||||||
let g = (i * 871295862 % 255) / 255.0; |
|
||||||
let b = (i * 287238767 % 255) / 255.0; |
|
||||||
|
|
||||||
if (r < 0.3) r = 0.3; |
|
||||||
if (g < 0.3) g = 0.3; |
|
||||||
if (b < 0.3) b = 0.3; |
|
||||||
|
|
||||||
positions.push(r, g, b); |
|
||||||
positions.push(r, g, b); |
|
||||||
positions.push(r, g, b); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const posf32 = new Float32Array(positions); |
|
||||||
const pointbytes = 4 * npoints * 2; |
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['in']); |
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, posf32.byteLength, gl.STATIC_DRAW); |
|
||||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, posf32.slice(0, npoints * 2)); |
|
||||||
gl.bufferSubData(gl.ARRAY_BUFFER, pointbytes, posf32.slice(npoints * 2)); |
|
||||||
|
|
||||||
{ |
|
||||||
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
|
||||||
const size = 2; // 2 components per iteration
|
|
||||||
const type = gl.FLOAT; // the data is 32bit floats
|
|
||||||
const normalize = false; // don't normalize the data
|
|
||||||
const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
|
||||||
const offset = 0; // start at the beginning of the buffer
|
|
||||||
gl.vertexAttribPointer(locations['a_pos'], size, type, normalize, stride, offset); |
|
||||||
} |
|
||||||
|
|
||||||
{ |
|
||||||
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
|
||||||
const size = 3; // 3 components per iteration
|
|
||||||
const type = gl.FLOAT; // the data is 32bit floats
|
|
||||||
const normalize = false; // don't normalize the data
|
|
||||||
const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
|
||||||
const offset = pointbytes; // start at the beginning of the buffer
|
|
||||||
gl.vertexAttribPointer(locations['a_triangle_color'], size, type, normalize, stride, offset); |
|
||||||
} |
|
||||||
|
|
||||||
{ |
|
||||||
const offset = 0; |
|
||||||
const count = npoints; |
|
||||||
gl.uniform3f(locations['u_color'], stroke_color.r, stroke_color.g, stroke_color.b); |
|
||||||
gl.uniform1i(locations['u_layer'], 0); |
|
||||||
gl.drawArrays(gl.TRIANGLES, offset, count); |
|
||||||
} |
|
||||||
|
|
||||||
window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes)); |
|
||||||
} |
} |
||||||
|
|
||||||
function main() { |
function main() { |
||||||
const canvas = document.querySelector('#c'); |
const state = { |
||||||
const gl = canvas.getContext('webgl'); |
'canvas': { |
||||||
|
'offset': { 'x': 0, 'y': 0 }, |
||||||
if (!gl) { |
'zoom': 1.0, |
||||||
console.error('FUCK!') |
}, |
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const vertex_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_shader_source); |
|
||||||
const fragment_shader = create_shader(gl, gl.FRAGMENT_SHADER, fragment_shader_source); |
|
||||||
const program = create_program(gl, vertex_shader, fragment_shader) |
|
||||||
|
|
||||||
const locations = {}; |
|
||||||
const buffers = {}; |
|
||||||
|
|
||||||
locations['a_pos'] = gl.getAttribLocation(program, 'a_pos'); |
|
||||||
locations['a_triangle_color'] = gl.getAttribLocation(program, 'a_triangle_color'); |
|
||||||
locations['u_res'] = gl.getUniformLocation(program, 'u_res'); |
|
||||||
locations['u_scale'] = gl.getUniformLocation(program, 'u_scale'); |
|
||||||
locations['u_translation'] = gl.getUniformLocation(program, 'u_translation'); |
|
||||||
locations['u_color'] = gl.getUniformLocation(program, 'u_color'); |
|
||||||
locations['u_layer'] = gl.getUniformLocation(program, 'u_layer'); |
|
||||||
|
|
||||||
buffers['in'] = gl.createBuffer(); |
|
||||||
|
|
||||||
const strokes = [ |
|
||||||
{ |
|
||||||
'points': [ |
|
||||||
{'x': 100, 'y': 100}, |
|
||||||
{'x': 105, 'y': 500}, |
|
||||||
{'x': 108, 'y': 140}, |
|
||||||
{'x': 508, 'y': 240}, |
|
||||||
] |
|
||||||
} |
|
||||||
]; |
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => { |
|
||||||
if (e.code === 'Space') { |
|
||||||
spacedown = true; |
|
||||||
} else if (e.code === 'KeyD') { |
|
||||||
debug_draw = !debug_draw; |
|
||||||
if (debug_draw) { |
|
||||||
stroke_color.r = 0.2; |
|
||||||
stroke_color.g = 0.2; |
|
||||||
stroke_color.b = 0.2; |
|
||||||
bgcolor.r = 0; |
|
||||||
bgcolor.g = 0; |
|
||||||
bgcolor.b = 0; |
|
||||||
} else { |
|
||||||
stroke_color.r = 0; |
|
||||||
stroke_color.g = 0; |
|
||||||
stroke_color.b = 0; |
|
||||||
bgcolor.r = 1; |
|
||||||
bgcolor.g = 1; |
|
||||||
bgcolor.b = 1; |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => { |
|
||||||
if (e.code === 'Space') { |
|
||||||
spacedown = false; |
|
||||||
moving = false; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('mousedown', (e) => { |
'cursor': { |
||||||
if (spacedown) { |
'x': 0, |
||||||
moving = true; |
'y': 0, |
||||||
return; |
}, |
||||||
} |
|
||||||
|
|
||||||
const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom; |
'moving': false, |
||||||
const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom; |
'drawing': false, |
||||||
|
'spacedown': false, |
||||||
|
|
||||||
current_stroke.length = 0; |
'stroke_width': 8, |
||||||
current_stroke.push({'x': x, 'y': y}); |
|
||||||
drawing = true; |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', (e) => { |
|
||||||
if (moving) { |
|
||||||
canvas_offset.x += e.movementX; |
|
||||||
canvas_offset.y += e.movementY; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (drawing) { |
'current_stroke': { |
||||||
const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom; |
'color': 0, |
||||||
const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom; |
'points': [], |
||||||
|
}, |
||||||
current_stroke.push({'x': x, 'y': y}); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('mouseup', (e) => { |
|
||||||
if (spacedown) { |
|
||||||
moving = false; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (drawing) { |
|
||||||
strokes.push({'points': process_stroke(current_stroke)}); |
|
||||||
current_stroke.length = 0; |
|
||||||
drawing = false; |
|
||||||
return; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
canvas.addEventListener('wheel', (e) => { |
|
||||||
const x = Math.round((e.clientX - canvas_offset.x) / canvas_zoom); |
|
||||||
const y = Math.round((e.clientY - canvas_offset.y) / canvas_zoom); |
|
||||||
|
|
||||||
const dz = (e.deltaY < 0 ? 0.1 : -0.1); |
|
||||||
const old_zoom = canvas_zoom; |
|
||||||
|
|
||||||
canvas_zoom *= (1.0 + dz); |
|
||||||
|
|
||||||
if (canvas_zoom > 100.0) { |
|
||||||
canvas_zoom = old_zoom; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (canvas_zoom < 0.2) { |
'strokes': [], |
||||||
canvas_zoom = old_zoom; |
}; |
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const zoom_offset_x = Math.round((dz * old_zoom) * x); |
const context = { |
||||||
const zoom_offset_y = Math.round((dz * old_zoom) * y); |
'canvas': null, |
||||||
|
'gl': null, |
||||||
|
'program': null, |
||||||
|
'buffers': {}, |
||||||
|
'locations': {}, |
||||||
|
'static_positions': [], |
||||||
|
'dynamic_positions': [], |
||||||
|
'static_colors': [], |
||||||
|
'dynamic_colors': [], |
||||||
|
'static_positions_f32': new Float32Array(0), |
||||||
|
'dynamic_positions_f32': new Float32Array(0), |
||||||
|
'static_colors_u8': new Uint8Array(0), |
||||||
|
'dynamic_colors_u8': new Uint8Array(0), |
||||||
|
'bgcolor': {'r': 0, 'g': 0, 'b': 0}, |
||||||
|
}; |
||||||
|
|
||||||
canvas_offset.x -= zoom_offset_x; |
init_webgl(state, context); |
||||||
canvas_offset.y -= zoom_offset_y; |
init_listeners(state, context); |
||||||
}); |
|
||||||
|
|
||||||
window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes)); |
window.requestAnimationFrame(() => draw(state, context)); |
||||||
} |
} |
@ -0,0 +1,115 @@ |
|||||||
|
function push_circle_at(positions, cl, r, g, b, c, o) { |
||||||
|
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[4].x, c.y + o[4].y, c.x + o[8].x, c.y + o[8].y); |
||||||
|
positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[0].x, c.y + o[0].y, c.x + o[2].x, c.y + o[2].y); |
||||||
|
positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[4].x, c.y + o[4].y, c.x + o[6].x, c.y + o[6].y); |
||||||
|
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[8].x, c.y + o[8].y, c.x + o[10].x, c.y + o[10].y); |
||||||
|
positions.push(c.x + o[2].x, c.y + o[2].y, c.x + o[0].x, c.y + o[0].y, c.x + o[1].x, c.y + o[1].y); |
||||||
|
positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[2].x, c.y + o[2].y, c.x + o[3].x, c.y + o[3].y); |
||||||
|
positions.push(c.x + o[6].x, c.y + o[6].y, c.x + o[4].x, c.y + o[4].y, c.x + o[5].x, c.y + o[5].y); |
||||||
|
positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[6].x, c.y + o[6].y, c.x + o[7].x, c.y + o[7].y); |
||||||
|
positions.push(c.x + o[10].x, c.y + o[10].y, c.x + o[8].x, c.y + o[8].y, c.x + o[9].x, c.y + o[9].y); |
||||||
|
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[10].x, c.y + o[10].y, c.x + o[11].x, c.y + o[11].y); |
||||||
|
|
||||||
|
for (let i = 0; i < 3 * 10; ++i) { |
||||||
|
cl.push(r, g, b); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function push_stroke(state, stroke, positions, colors) { |
||||||
|
const stroke_width = state.stroke_width; |
||||||
|
const points = stroke.points; |
||||||
|
const color_u32 = stroke.color; |
||||||
|
|
||||||
|
const r = (color_u32 >> 16) & 0xFF; |
||||||
|
const g = (color_u32 >> 8) & 0xFF; |
||||||
|
const b = color_u32 & 0xFF; |
||||||
|
|
||||||
|
if (points.length < 2) { |
||||||
|
// TODO
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Simple 12 point circle (store offsets and reuse)
|
||||||
|
const POINTS = 12; |
||||||
|
const phi_step = 2 * Math.PI / POINTS; |
||||||
|
|
||||||
|
const circle_offsets = []; |
||||||
|
|
||||||
|
for (let i = 0; i < POINTS; ++i) { |
||||||
|
const phi = phi_step * i; |
||||||
|
const ox = stroke_width / 2 * Math.cos(phi); |
||||||
|
const oy = stroke_width / 2 * Math.sin(phi); |
||||||
|
circle_offsets.push({'x': ox, 'y': oy}); |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; ++i) { |
||||||
|
const px = points[i].x; |
||||||
|
const py = points[i].y; |
||||||
|
|
||||||
|
const nextpx = points[i + 1].x; |
||||||
|
const nextpy = points[i + 1].y; |
||||||
|
|
||||||
|
const d1x = nextpx - px; |
||||||
|
const d1y = nextpy - py; |
||||||
|
|
||||||
|
// Perpendicular to (d1x, d1y), points to the LEFT
|
||||||
|
let perp1x = -d1y; |
||||||
|
let perp1y = d1x; |
||||||
|
|
||||||
|
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); |
||||||
|
|
||||||
|
perp1x /= perpnorm1; |
||||||
|
perp1y /= perpnorm1; |
||||||
|
|
||||||
|
const s1x = px + perp1x * stroke_width / 2; |
||||||
|
const s1y = py + perp1y * stroke_width / 2; |
||||||
|
const s2x = px - perp1x * stroke_width / 2; |
||||||
|
const s2y = py - perp1y * stroke_width / 2; |
||||||
|
|
||||||
|
const s3x = nextpx + perp1x * stroke_width / 2; |
||||||
|
const s3y = nextpy + perp1y * stroke_width / 2; |
||||||
|
const s4x = nextpx - perp1x * stroke_width / 2; |
||||||
|
const s4y = nextpy - perp1y * stroke_width / 2; |
||||||
|
|
||||||
|
positions.push(s1x, s1y, s2x, s2y, s4x, s4y); |
||||||
|
positions.push(s1x, s1y, s4x, s4y, s3x, s3y); |
||||||
|
|
||||||
|
for (let j = 0; j < 6; ++j) { |
||||||
|
colors.push(r, g, b); |
||||||
|
} |
||||||
|
|
||||||
|
// Rotate circle offsets so that the diameter of the circle is
|
||||||
|
// perpendicular to the (dx, dy) vector. This way the circle won't
|
||||||
|
// "poke out" of the rectangle
|
||||||
|
const angle = Math.atan(Math.abs(s3x - s4x), Math.abs(s3y - s4y)); |
||||||
|
|
||||||
|
push_circle_at(positions, colors, r, g, b, points[i], circle_offsets); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: angle
|
||||||
|
push_circle_at(positions, colors, r, g, b, points[points.length - 1], circle_offsets); |
||||||
|
} |
||||||
|
|
||||||
|
function add_static_stroke(state, context, stroke) { |
||||||
|
state.strokes.push(stroke); |
||||||
|
push_stroke(state, stroke, context.static_positions, context.static_colors); |
||||||
|
context.static_positions_f32 = new Float32Array(context.static_positions); |
||||||
|
context.static_colors_u8 = new Uint8Array(context.static_colors); |
||||||
|
} |
||||||
|
|
||||||
|
function update_dynamic_stroke(state, context, point) { |
||||||
|
state.current_stroke.points.push(point); |
||||||
|
context.dynamic_positions.length = 0; // TODO: incremental
|
||||||
|
context.dynamic_colors.length = 0; |
||||||
|
push_stroke(state, state.current_stroke, context.dynamic_positions, context.dynamic_colors); |
||||||
|
context.dynamic_positions_f32 = new Float32Array(context.dynamic_positions); |
||||||
|
context.dynamic_colors_u8 = new Uint8Array(context.dynamic_colors); |
||||||
|
} |
||||||
|
|
||||||
|
function clear_dynamic_stroke(state, context) { |
||||||
|
state.current_stroke.points.length = 0; |
||||||
|
context.dynamic_positions.length = 0; |
||||||
|
context.dynamic_colors.length = 0; |
||||||
|
context.dynamic_positions_f32 = new Float32Array(0); |
||||||
|
context.dynamic_colors_u8 = new Uint8Array(0); |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
function init_listeners(state, context) { |
||||||
|
window.addEventListener('keydown', (e) => keydown(e, state)); |
||||||
|
window.addEventListener('keyup', (e) => keyup(e, state)); |
||||||
|
|
||||||
|
context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context)); |
||||||
|
context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context)); |
||||||
|
context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context)); |
||||||
|
context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); |
||||||
|
} |
||||||
|
|
||||||
|
function keydown(e, state) { |
||||||
|
if (e.code === 'Space') { |
||||||
|
state.spacedown = true; |
||||||
|
} else if (e.code === 'KeyD') { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function keyup(e, state) { |
||||||
|
if (e.code === 'Space') { |
||||||
|
state.spacedown = false; |
||||||
|
state.moving = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mousedown(e, state, context) { |
||||||
|
if (state.spacedown) { |
||||||
|
state.moving = true; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom; |
||||||
|
const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom; |
||||||
|
|
||||||
|
clear_dynamic_stroke(state, context); |
||||||
|
update_dynamic_stroke(state, context, {'x': x, 'y': y}); |
||||||
|
state.drawing = true; |
||||||
|
|
||||||
|
window.requestAnimationFrame(() => draw(state, context)); |
||||||
|
} |
||||||
|
|
||||||
|
function mousemove(e, state, context) { |
||||||
|
let do_draw = false; |
||||||
|
|
||||||
|
if (state.moving) { |
||||||
|
state.canvas.offset.x += e.movementX; |
||||||
|
state.canvas.offset.y += e.movementY; |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.drawing) { |
||||||
|
const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom; |
||||||
|
const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom; |
||||||
|
update_dynamic_stroke(state, context, {'x': x, 'y': y}); |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (do_draw) { |
||||||
|
window.requestAnimationFrame(() => draw(state, context)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mouseup(e, state, context) { |
||||||
|
if (state.spacedown) { |
||||||
|
state.moving = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.drawing) { |
||||||
|
const stroke = { |
||||||
|
'color': Math.round(Math.random() * 4294967295), |
||||||
|
'points': process_stroke(state.current_stroke.points) |
||||||
|
}; |
||||||
|
|
||||||
|
add_static_stroke(state, context, stroke); |
||||||
|
clear_dynamic_stroke(state, context); |
||||||
|
state.drawing = false; |
||||||
|
|
||||||
|
window.requestAnimationFrame(() => draw(state, context)); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function wheel(e, state, context) { |
||||||
|
const x = Math.round((e.clientX - state.canvas.offset.x) / state.canvas.zoom); |
||||||
|
const y = Math.round((e.clientY - state.canvas.offset.y) / state.canvas.zoom); |
||||||
|
|
||||||
|
const dz = (e.deltaY < 0 ? 0.1 : -0.1); |
||||||
|
const old_zoom = state.canvas.zoom; |
||||||
|
|
||||||
|
state.canvas.zoom *= (1.0 + dz); |
||||||
|
|
||||||
|
if (state.canvas.zoom > 100.0) { |
||||||
|
state.canvas.zoom = old_zoom; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.canvas.zoom < 0.2) { |
||||||
|
state.canvas.zoom = old_zoom; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const zoom_offset_x = Math.round((dz * old_zoom) * x); |
||||||
|
const zoom_offset_y = Math.round((dz * old_zoom) * y); |
||||||
|
|
||||||
|
state.canvas.offset.x -= zoom_offset_x; |
||||||
|
state.canvas.offset.y -= zoom_offset_y; |
||||||
|
|
||||||
|
window.requestAnimationFrame(() => draw(state, context)); |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
const vertex_shader_source = ` |
||||||
|
attribute vec2 a_pos; |
||||||
|
attribute vec3 a_color; |
||||||
|
|
||||||
|
uniform vec2 u_scale; |
||||||
|
uniform vec2 u_res; |
||||||
|
uniform vec2 u_translation; |
||||||
|
uniform int u_layer; |
||||||
|
|
||||||
|
varying vec3 v_color; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
||||||
|
vec2 screen02 = screen01 * 2.0; |
||||||
|
screen02.y = 2.0 - screen02.y; |
||||||
|
vec2 screen11 = screen02 - 1.0; |
||||||
|
v_color = a_color; |
||||||
|
gl_Position = vec4(screen11, u_layer, 1); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const fragment_shader_source = ` |
||||||
|
precision mediump float; |
||||||
|
|
||||||
|
varying vec3 v_color; |
||||||
|
|
||||||
|
void main() { |
||||||
|
gl_FragColor = vec4(v_color, 1); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
function init_webgl(state, context) { |
||||||
|
context.canvas = document.querySelector('#c'); |
||||||
|
context.gl = context.canvas.getContext('webgl', { |
||||||
|
'preserveDrawingBuffer': true, |
||||||
|
'desynchronized': true, |
||||||
|
'antialias': true, |
||||||
|
}); |
||||||
|
|
||||||
|
const vertex_shader = create_shader(context.gl, context.gl.VERTEX_SHADER, vertex_shader_source); |
||||||
|
const fragment_shader = create_shader(context.gl, context.gl.FRAGMENT_SHADER, fragment_shader_source); |
||||||
|
const program = create_program(context.gl, vertex_shader, fragment_shader) |
||||||
|
|
||||||
|
context.program = program; |
||||||
|
|
||||||
|
context.locations['a_pos'] = context.gl.getAttribLocation(program, 'a_pos'); |
||||||
|
context.locations['a_color'] = context.gl.getAttribLocation(program, 'a_color'); |
||||||
|
|
||||||
|
context.locations['u_res'] = context.gl.getUniformLocation(program, 'u_res'); |
||||||
|
context.locations['u_scale'] = context.gl.getUniformLocation(program, 'u_scale'); |
||||||
|
context.locations['u_translation'] = context.gl.getUniformLocation(program, 'u_translation'); |
||||||
|
context.locations['u_layer'] = context.gl.getUniformLocation(program, 'u_layer'); |
||||||
|
|
||||||
|
context.buffers['b_pos'] = context.gl.createBuffer(); |
||||||
|
context.buffers['b_color'] = context.gl.createBuffer(); |
||||||
|
|
||||||
|
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; |
||||||
|
|
||||||
|
window.requestAnimationFrame(() => 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)) { |
||||||
|
return program; |
||||||
|
} |
||||||
|
|
||||||
|
console.error('link:', gl.getProgramInfoLog(program)); |
||||||
|
|
||||||
|
gl.deleteProgram(program); |
||||||
|
} |
Loading…
Reference in new issue