Browse Source

Dynamic cursor that shows size and color of the brush. Background pattern scaffolding

ssao
A.Olokhtonov 6 months ago
parent
commit
07bb47b6dc
  1. 1
      client/client_recv.js
  2. 18
      client/default.css
  3. 281
      client/icons/crosshair.svg
  4. 5
      client/index.html
  5. 22
      client/tools.js
  6. 44
      client/webgl_draw.js
  7. 33
      client/webgl_listeners.js
  8. 46
      client/webgl_shaders.js

1
client/client_recv.js

@ -484,6 +484,7 @@ async function handle_message(state, context, d) { @@ -484,6 +484,7 @@ async function handle_message(state, context, d) {
console.timeEnd('init');
update_cursor(state);
draw_html(state);
break;

18
client/default.css

@ -40,7 +40,7 @@ canvas { @@ -40,7 +40,7 @@ canvas {
width: 100%;
height: 100%;
display: block;
/* */
cursor: url('icons/crosshair.svg') 16 16, crosshair;
}
canvas.picker {
@ -59,6 +59,14 @@ canvas.mousemoving { @@ -59,6 +59,14 @@ canvas.mousemoving {
cursor: move;
}
.brush-dom {
position: absolute;
pointer-events: none;
user-select: none;
top: 0;
left: 0;
}
.html-hud {
position: fixed;
top: 0;
@ -344,14 +352,6 @@ canvas.mousemoving { @@ -344,14 +352,6 @@ canvas.mousemoving {
}
}
#stroke-preview {
position: absolute;
border-radius: 50%;
left: 50%;
top: 96px;
transform: translate(-50%, -50%);
}
.offline-toast {
position: fixed;
top: 50%;

281
client/icons/crosshair.svg

@ -0,0 +1,281 @@ @@ -0,0 +1,281 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 32 32"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="crosshair.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#5fa9cd"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="19.02887"
inkscape:cx="10.011104"
inkscape:cy="17.447173"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid686" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
id="linearGradient1226"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop1224" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1396"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7,-19)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1465"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7,-36)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1471"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25,-36)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1473"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25,-19)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2232"
x="-1.1"
y="-0.275"
width="3.2"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2222" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2224" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2226" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2228" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2230" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2244"
x="-1.1"
y="-0.275"
width="3.2"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2234" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2236" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2238" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2240" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2242" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2256"
x="-1.0999969"
y="-0.275"
width="3.1999937"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2246" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2248" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2250" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2252" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2254" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2268"
x="-1.1"
y="-0.275"
width="3.2"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2258" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2260" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2262" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2264" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2266" />
</filter>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1396);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2232)"
id="rect1390"
width="2"
height="8"
x="15"
y="-12"
rx="0"
ry="0"
transform="rotate(90)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1465);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2268)"
id="rect1463"
width="2"
height="8"
x="15"
y="-28"
rx="0"
ry="0"
transform="rotate(90)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1473);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2244)"
id="rect1467"
width="2"
height="8"
x="-17"
y="-12"
rx="0"
ry="0"
transform="scale(-1)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1471);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2256)"
id="rect1469"
width="2.0000057"
height="8"
x="-17"
y="-28"
rx="0"
ry="0"
transform="scale(-1)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

5
client/index.html

@ -27,14 +27,12 @@ @@ -27,14 +27,12 @@
<script type="text/javascript" src="client_send.js"></script>
<script type="text/javascript" src="client_recv.js"></script>
<script type="text/javascript" src="websocket.js"></script>
<style id="cursor-style">
</style>
</head>
<body>
<div class="main">
<canvas id="c"></canvas>
<div class="html-hud"></div>
<div class="brush-dom"></div>
<div class="debug-window dhide">
<div id="debug-stats" class="flexcol"></div>
@ -69,7 +67,6 @@ @@ -69,7 +67,6 @@
<div class="sizer">
<input type="range" class="slider" id="stroke-width" min="0.01" step="0.01" max="64">
</div>
<div id="stroke-preview" class="dhide"></div>
<div class="player-list vhide"></div>
</div>

22
client/tools.js

@ -71,37 +71,17 @@ function set_color_u32(state, color_u32) { @@ -71,37 +71,17 @@ function set_color_u32(state, color_u32) {
select_color(state, major_color, color_u32);
state.players[state.me].color = color_u32
update_cursor(state);
fire_event(state, color_event(color_u32));
}
function show_stroke_preview(state, size) {
const preview = document.querySelector('#stroke-preview');
preview.style.width = size * state.canvas.zoom + 'px';
preview.style.height = size * state.canvas.zoom + 'px';
preview.style.background = color_from_u32(state.players[state.me].color);
preview.classList.remove('dhide');
}
function hide_stroke_preview() {
document.querySelector('#stroke-preview').classList.add('dhide');
}
function switch_stroke_width(e, state) {
if (!state.online) return;
const value = parseInt(e.target.value);
state.players[state.me].width = value;
show_stroke_preview(state, value);
update_cursor(state);
if (state.hide_preview) {
clearTimeout(state.hide_preview);
}
state.hide_preview = setTimeout(hide_stroke_preview, config.brush_preview_timeout);
}
function broadcast_stroke_width(e, state) {

44
client/webgl_draw.js

@ -75,6 +75,7 @@ function draw_html(state) { @@ -75,6 +75,7 @@ function draw_html(state) {
}
}
async function draw(state, context) {
const cpu_before = performance.now();
@ -82,9 +83,6 @@ async function draw(state, context) { @@ -82,9 +83,6 @@ async function draw(state, context) {
const width = window.innerWidth;
const height = window.innerHeight;
locations = context.locations['sdf'].main;
buffers = context.buffers['sdf'];
bvh_clip(state, context);
const segment_count = await geometry_write_instances(state, context);
@ -104,7 +102,34 @@ async function draw(state, context) { @@ -104,7 +102,34 @@ async function draw(state, context) {
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Draw the background pattern
gl.useProgram(context.programs['pattern'].dots);
buffers = context.buffers['pattern'];
locations = context.locations['pattern'].dots;
{
// Reused data
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_dot']);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([10, 0, 20, 0, 10, 10]), gl.STREAM_DRAW);
gl.enableVertexAttribArray(locations['a_xy']);
gl.vertexAttribPointer(locations['a_xy'], 2, gl.FLOAT, false, 2 * 4, 0);
// Per-instance data
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([100, 100, 150, 150, 10, 10, 200, 10]), gl.STREAM_DRAW);
gl.enableVertexAttribArray(locations['a_center']);
gl.vertexAttribPointer(locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribDivisor(locations['a_center'], 1);
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 4);
}
gl.useProgram(context.programs['sdf'].main);
buffers = context.buffers['sdf'];
locations = context.locations['sdf'].main;
// "Static" data upload
if (segment_count > 0) {
@ -147,8 +172,14 @@ async function draw(state, context) { @@ -147,8 +172,14 @@ async function draw(state, context) {
// Static draw (everything already bound)
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segment_count);
}
// I don't really know why I need to do this, but it
// makes background patter drawcall work properly
gl.vertexAttribDivisor(locations['a_a'], 0);
gl.vertexAttribDivisor(locations['a_b'], 0);
gl.vertexAttribDivisor(locations['a_stroke_id'], 0);
gl.vertexAttribDivisor(locations['a_pressure'], 0);
}
// Dynamic strokes should be drawn above static strokes
gl.clear(gl.DEPTH_BUFFER_BIT);
@ -190,6 +221,11 @@ async function draw(state, context) { @@ -190,6 +221,11 @@ async function draw(state, context) {
gl.vertexAttribDivisor(locations['a_pressure'], 1);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dynamic_segment_count);
gl.vertexAttribDivisor(locations['a_a'], 0);
gl.vertexAttribDivisor(locations['a_b'], 0);
gl.vertexAttribDivisor(locations['a_stroke_id'], 0);
gl.vertexAttribDivisor(locations['a_pressure'], 0);
}
document.getElementById('debug-stats').innerHTML = `

33
client/webgl_listeners.js

@ -182,6 +182,7 @@ function mousedown(e, state, context) { @@ -182,6 +182,7 @@ function mousedown(e, state, context) {
if (state.colorpicking) {
const color_u32 = color_to_u32(state.color_picked.substring(1));
state.players[state.me].color = color_u32;
update_cursor(state);
fire_event(state, color_event(color_u32));
return;
}
@ -243,6 +244,14 @@ function mousemove(e, state, context) { @@ -243,6 +244,14 @@ function mousemove(e, state, context) {
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp);
if (state.me in state.players) {
const me = state.players[state.me];
const width = Math.max(me.width * state.canvas.zoom, 2.0);
const brush_x = screenp.x - width / 2 - 2;
const brush_y = screenp.y - width / 2 - 2;
document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`;
}
if (state.me in state.players && dist_v2(state.players[state.me].cursor, canvasp) > 5) {
state.players[state.me].cursor = canvasp;
fire_event(state, movecursor_event(canvasp.x, canvasp.y));
@ -352,20 +361,32 @@ function mouseup(e, state, context) { @@ -352,20 +361,32 @@ function mouseup(e, state, context) {
}
function mouseleave(e, state, context) {
if (state.moving) {
state.moving = false;
context.canvas.classList.remove('movemode');
}
exit_picker_mode(state);
// something else?
}
function update_cursor(state) {
const style = document.querySelector('#cursor-style');
const width = Math.max(state.players[state.me].width * state.canvas.zoom, 2.0);
const me = state.players[state.me];
const width = Math.max(me.width * state.canvas.zoom, 2.0);
const radius = width / 2;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width * 2 + 2}" height="${width * 2 + 2}">
<circle cx="${radius + 1}" cy="${radius + 1}" r="${radius}" stroke="white" fill="none" stroke-width="2"/>
<circle cx="${radius + 1}" cy="${radius + 1}" r="${radius}" stroke="black" fill="none" stroke-width="1"/>
const current_color = color_from_u32(me.color);
const stroke = (me.color === 0xFFFFFF ? 'black' : 'white');
const svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}">
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/>
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/>
</svg>`.replaceAll('\n', ' ');
style.innerText = `canvas { cursor: url('data:image/svg+xml;utf8,${svg}') ${radius + 1} ${radius + 1}, crosshair; }`;
document.querySelector('.brush-dom').innerHTML = svg;
const brush_x = state.cursor.x - width / 2 - 2;
const brush_y = state.cursor.y - width / 2 - 2;
document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`;
}
function wheel(e, state, context) {

46
client/webgl_shaders.js

@ -249,6 +249,30 @@ const tquad_fs_src = `#version 300 es @@ -249,6 +249,30 @@ const tquad_fs_src = `#version 300 es
}
`;
const dots_vs_src = `#version 300 es
in vec2 a_xy;
in vec2 a_center; // per-instance
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
void main() {
vec2 screen02 = ((a_center + a_xy) * u_scale + u_translation) / u_res * 2.0;
screen02.y = 2.0 - screen02.y;
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
}
`;
const dots_fs_src = `#version 300 es
precision highp float;
layout(location = 0) out vec4 FragColor;
void main() {
FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
`;
function init_webgl(state, context) {
context.canvas = document.querySelector('#c');
context.gl = context.canvas.getContext('webgl2', {
@ -282,12 +306,18 @@ function init_webgl(state, context) { @@ -282,12 +306,18 @@ function init_webgl(state, context) {
const simple_vs = create_shader(gl, gl.VERTEX_SHADER, simple_vs_src);
const simple_fs = create_shader(gl, gl.FRAGMENT_SHADER, simple_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);
context.programs['image'] = create_program(gl, quad_vs, quad_fs);
context.programs['debug'] = create_program(gl, simple_vs, simple_fs);
context.programs['sdf'] = {
'opaque': create_program(gl, opaque_vs, nop_fs),
'main': create_program(gl, sdf_vs, sdf_fs),
};
context.programs['pattern'] = {
'dots': create_program(gl, dots_vs, dots_fs),
};
context.locations['debug'] = {
'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'),
@ -325,6 +355,17 @@ function init_webgl(state, context) { @@ -325,6 +355,17 @@ function init_webgl(state, context) {
}
};
context.locations['pattern'] = {
'dots': {
'a_xy': gl.getAttribLocation(context.programs['pattern'].dots, 'a_xy'),
'a_center': gl.getAttribLocation(context.programs['pattern'].dots, 'a_center'),
'u_res': gl.getUniformLocation(context.programs['pattern'].dots, 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['pattern'].dots, 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['pattern'].dots, 'u_translation'),
}
};
context.buffers['debug'] = {
'b_packed': gl.createBuffer(),
};
@ -334,6 +375,11 @@ function init_webgl(state, context) { @@ -334,6 +375,11 @@ function init_webgl(state, context) {
'b_dynamic_instance': gl.createBuffer(),
};
context.buffers['pattern'] = {
'b_instance': gl.createBuffer(),
'b_dot': gl.createBuffer(),
};
context.textures = {
'stroke_data': gl.createTexture(),
'dynamic_stroke_data': gl.createTexture(),

Loading…
Cancel
Save