Compare commits

..

6 Commits
sdf ... master

Author SHA1 Message Date
Aleksey Olokhtonov d6f2fc5548 Remove subframe logic, instead use ffmpeg to blur frames afterwards 2 months ago
Aleksey Olokhtonov 0384ac6246 Render button! 2 months ago
Aleksey Olokhtonov db277039df Make vertial zooming faster and also framerate-independent. 2 months ago
Aleksey Olokhtonov 268a55b71b Use framerate-independent lerp. Very Freya-esque in that sense. 2 months ago
Aleksey Olokhtonov 26ec2216ce Unstuck from picker mode on mousemove with no ctrl. 2 months ago
Aleksey Olokhtonov dc1553dc1e Increase WASM mem limit to 256MB. Rename aux.js to not get adblocked. 2 months ago
  1. 22
      README.txt
  2. 3
      client/config.js
  3. 4
      client/index.html
  4. 0
      client/random_helpers.js
  5. 4
      client/speed.js
  6. 23
      client/webgl_draw.js
  7. 169
      client/webgl_listeners.js
  8. 16
      client/webgl_shaders.js

22
README.txt

@ -10,8 +10,7 @@ Release:
+ Multithreading for LOD + Multithreading for LOD
+ Textured quads (pictures, code already written in older version + Textured quads (pictures, code already written in older version
+ Resize and move pictures (draw handles) + Resize and move pictures (draw handles)
- Z-prepass fringe bug (also, when do we enable the prepass?) + Frame-independent lerp where applicable
- Frame-independent lerp where applicable
- Dedup images on server, so we don't download the same image too many times - Dedup images on server, so we don't download the same image too many times
- No lag while downloading images - No lag while downloading images
+ Bugs + Bugs
@ -83,10 +82,7 @@ Bonus:
+ Migrate old non-pressure desks + Migrate old non-pressure desks
+ Check out e.pressure on touch devices + Check out e.pressure on touch devices
- Send pressure in PREDRAW event - Send pressure in PREDRAW event
- Stroke smoothing + Stroke smoothing
https://github.com/xournalpp/xournalpp/issues/2320
https://www.digital-epigraphy.com/tutorials/the-most-useful-new-features-of-photoshop-cc-using-brush-stroke-smoothing-for-digital-inking
https://stackoverflow.com/questions/20618804/how-to-smooth-a-curve-for-a-dataset
- Curve modification - Curve modification
- Select curves (with a lasso?) - Select curves (with a lasso?)
- Move whole curve - Move whole curve
@ -104,3 +100,17 @@ Bonus:
Bonus-bonus: Bonus-bonus:
- Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?) - Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?)
Shiplist:
- Fix mess
- Redo
- User-friendly config
+ min 1px wide lines with transparency fade
- offline-only version
- select and manupulate curves with selection tool
- deduplicate pictures
- No lag while downloading images
- Show previous color in color picker preview
- Color picker should work for ruler
- Color picker precision and pick order seems off
+ Stuck in color picker mode when mouse leaves screen

3
client/config.js

@ -6,6 +6,9 @@ const config = {
ws_reconnect_timeout: 2000, ws_reconnect_timeout: 2000,
brush_preview_timeout: 1000, brush_preview_timeout: 1000,
second_finger_timeout: 500, second_finger_timeout: 500,
// animation_decay: 16,
animation_decay: 10,
vertical_zoom_speed_multiplier: 3,
debug_print: false, debug_print: false,
draw_bvh: false, draw_bvh: false,
draw_fullnodes: false, draw_fullnodes: false,

4
client/index.html

@ -11,7 +11,7 @@
<!-- <link rel="preload" href="icons/picker.svg" as="image" type="image/svg+xml" /> --> <!-- <link rel="preload" href="icons/picker.svg" as="image" type="image/svg+xml" /> -->
<script type="text/javascript" src="aux.js"></script> <script type="text/javascript" src="random_helpers.js"></script>
<script type="text/javascript" src="heapify.js"></script> <script type="text/javascript" src="heapify.js"></script>
<script type="text/javascript" src="bvh.js"></script> <script type="text/javascript" src="bvh.js"></script>
<script type="text/javascript" src="math.js"></script> <script type="text/javascript" src="math.js"></script>
@ -47,6 +47,7 @@
<label><input type="checkbox" id="debug-print">Debug print</label> <label><input type="checkbox" id="debug-print">Debug print</label>
<label><input type="checkbox" id="draw-bvh">Show BVH</label> <label><input type="checkbox" id="draw-bvh">Show BVH</label>
<button id="debug-render">Render</button>
<button id="debug-begin-benchmark" title="Do not forget to enable recording in your browser!">Benchmark</button> <button id="debug-begin-benchmark" title="Do not forget to enable recording in your browser!">Benchmark</button>
</div> </div>
@ -151,7 +152,6 @@
<div class="color-minor" data-color="1864ab"><div class="color-pane" style="background: #1864ab;"></div></div> <div class="color-minor" data-color="1864ab"><div class="color-pane" style="background: #1864ab;"></div></div>
</div> </div>
<div class="color-major"> <div class="color-major">
<div class="color-minor" data-color="e3fafc"><div class="color-pane" style="background: #e3fafc;"></div></div> <div class="color-minor" data-color="e3fafc"><div class="color-pane" style="background: #e3fafc;"></div></div>
<div class="color-minor" data-color="c5f6fa"><div class="color-pane" style="background: #c5f6fa;"></div></div> <div class="color-minor" data-color="c5f6fa"><div class="color-pane" style="background: #c5f6fa;"></div></div>
<div class="color-minor" data-color="99e9f2"><div class="color-pane" style="background: #99e9f2;"></div></div> <div class="color-minor" data-color="99e9f2"><div class="color-pane" style="background: #99e9f2;"></div></div>

0
client/aux.js → client/random_helpers.js

4
client/speed.js

@ -36,8 +36,8 @@ function workers_thread_message(workers, message, thread_field=null) {
async function init_wasm(state) { async function init_wasm(state) {
const memory = new WebAssembly.Memory({ const memory = new WebAssembly.Memory({
initial: 2048, // F U initial: 4096, // F U
maximum: 2048, // 128MiB maximum: 4096, // 256MiB
shared: true, shared: true,
}); });

23
client/webgl_draw.js

@ -235,7 +235,7 @@ function draw_strokes(state, width, height, programs, gl, lod_levels, segment_co
tv_pop(batches_tv); tv_pop(batches_tv);
} }
async function draw(state, context, animate, ts) { async function draw(state, context, animate, ts, external_draw = false) {
const dt = ts - context.last_frame_ts; const dt = ts - context.last_frame_ts;
const cpu_before = performance.now(); const cpu_before = performance.now();
@ -584,7 +584,7 @@ async function draw(state, context, animate, ts) {
<span>Total vertices: ${stat_total_vertices}</span> <span>Total vertices: ${stat_total_vertices}</span>
<span>Canvas offset: (${Math.round(state.canvas.offset.x * 100) / 100}, ${Math.round(state.canvas.offset.y * 100) / 100})</span> <span>Canvas offset: (${Math.round(state.canvas.offset.x * 100) / 100}, ${Math.round(state.canvas.offset.y * 100) / 100})</span>
<span>Canvas zoom level: ${state.canvas.zoom_level}</span> <span>Canvas zoom level: ${state.canvas.zoom_level}</span>
<span>Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}</span>`; <span>Canvas zoom: ${Math.round(state.canvas.zoom * 10000) / 10000}</span>`;
if (context.gpu_timer_ext) { if (context.gpu_timer_ext) {
gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT); gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT);
@ -630,18 +630,27 @@ async function draw(state, context, animate, ts) {
} }
if (state.canvas.target_zoom != state.canvas.zoom) { if (state.canvas.target_zoom != state.canvas.zoom) {
update_canvas_zoom(state, state.canvas.zoom, state.canvas.target_zoom, animate ? dt : context.last_frame_dt); update_canvas_zoom(state, state.canvas.zoom, state.canvas.target_zoom, animate ? dt : 0);
schedule_draw(state, context, true); if (!external_draw) {
schedule_draw(state, context, true);
}
} }
}
context.last_frame_dt = dt; // https://www.youtube.com/watch?v=LSNQuFEDOyQ
function exp_decay(a, b, decay, dt) {
return b + (a - b) * Math.exp(-decay * dt);
} }
function update_canvas_zoom(state, current, target, dt) { function update_canvas_zoom(state, current, target, dt) {
const rate = Math.min(1.0, dt / 16.66 * 0.3); let decay = config.animation_decay;
if (state.zoomdown) {
decay *= config.vertical_zoom_speed_multiplier; // to make it feel more responsive at fast speed
}
if (Math.abs(1.0 - current / target) > 0.01) { if (Math.abs(1.0 - current / target) > 0.01) {
state.canvas.zoom = current + (target - current) * rate; state.canvas.zoom = exp_decay(state.canvas.zoom, target, decay, dt / 1000.0);
} else { } else {
state.canvas.zoom = target; state.canvas.zoom = target;
} }

169
client/webgl_listeners.js

@ -6,8 +6,7 @@ function init_listeners(state, context) {
context.canvas.addEventListener('pointerdown', (e) => pointerdown(e, state, context)); context.canvas.addEventListener('pointerdown', (e) => pointerdown(e, state, context));
context.canvas.addEventListener('pointermove', (e) => pointermove(e, state, context)); context.canvas.addEventListener('pointermove', (e) => pointermove(e, state, context));
context.canvas.addEventListener('pointerup', (e) => pointerup(e, state, context)); context.canvas.addEventListener('pointerup', (e) => pointerup(e, state, context));
context.canvas.addEventListener('pointercancel', (e) => pointerup(e, state, context)); context.canvas.addEventListener('pointercancel', (e) => pointercancel(e, state, context));
//context.canvas.addEventListener('pointerleave', (e) => pointerup(e, state, context));
context.canvas.addEventListener('pointerleave', (e) => pointerleave(e, state, context)); context.canvas.addEventListener('pointerleave', (e) => pointerleave(e, state, context));
context.canvas.addEventListener('contextmenu', cancel); context.canvas.addEventListener('contextmenu', cancel);
context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); context.canvas.addEventListener('wheel', (e) => wheel(e, state, context));
@ -42,6 +41,141 @@ function debug_panel_init(state, context) {
schedule_draw(state, context); schedule_draw(state, context);
}); });
document.getElementById('debug-render').addEventListener('click', async (e) => {
const encoded_chunks = [];
let total_chunk_bytes = 0;
const handle_encoded_chunk = (chunk, metadata) => {
encoded_chunks.push(chunk);
total_chunk_bytes += chunk.byteLength;
};
const canvas = document.getElementById('c');
const init = {
output: handle_encoded_chunk,
error: (e) => {
console.log(e.message);
},
};
const render_framerate = 240;
const render_frames = 720;
const start_movement_frame = 240;
const start_movement2_frame = 480;
let updated_zoom = false;
const encoder_config = {
codec: "vp8",
width: canvas.width,
height: canvas.height,
displayWidth: canvas.width,
displayHeight: canvas.height,
bitrate: 100_000_000,
framerate: render_framerate,
};
const { supported } = await VideoEncoder.isConfigSupported(encoder_config);
if (supported) {
const encoder = new VideoEncoder(init);
encoder.configure(encoder_config);
let zoom_level = 5;
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom_screenp = {'x': canvas.width / 2, 'y': canvas.height / 2};
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
for (let i = 0; i < render_frames; ++i) {
const time_now = i / render_framerate;
await draw(state, context, i > 0, time_now * 1000, true);
const frame = new VideoFrame(canvas, { timestamp: time_now });
encoder.encode(frame, {keyFrame: true});
frame.close();
await encoder.flush();
if (start_movement_frame <= i && i < start_movement2_frame) {
state.canvas.offset.x += (1000 - state.canvas.offset.x) * 0.05;
state.canvas.offset.y += (1000 - state.canvas.offset.y) * 0.05;
}
if (i > start_movement2_frame) {
if (!updated_zoom) {
zoom_level = -25;
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
updated_zoom = true;
}
//state.canvas.offset.x += (3500 - state.canvas.offset.x) * 0.05;
//state.canvas.offset.y += (5000 - state.canvas.offset.y) * 0.05;
}
}
const data = new Uint8Array(32 + render_frames * 12 + total_chunk_bytes);
data[0] = 0x44;
data[1] = 0x4B;
data[2] = 0x49;
data[3] = 0x46;
data[6] = 32;
data[8] = 0x56;
data[9] = 0x50;
data[10] = 0x38;
data[11] = 0x30;
data[12] = encoder_config.width & 0xFF;
data[13] = (encoder_config.width >> 8) & 0xFF;
data[14] = encoder_config.height & 0xFF;
data[15] = (encoder_config.height >> 8) & 0xFF;
data[16] = render_framerate; // timebase denom
data[20] = 1; // timebase numenator..?
data[24] = render_frames; // frame count
let offset = 32;
for (let i = 0; i < render_frames; ++i) {
const chunk = encoded_chunks[i];
// frame header
// frame length
data[offset + 0] = chunk.byteLength & 0xFF;
data[offset + 1] = (chunk.byteLength >> 8) & 0xFF;
data[offset + 2] = (chunk.byteLength >> 16) & 0xFF;
data[offset + 3] = (chunk.byteLength >> 24) & 0xFF;
// PTS
// set to frame number for now
data[offset + 4] = i & 0xFF;
data[offset + 5] = (i >> 8) & 0xFF;
data[offset + 6] = (i >> 16) & 0xFF;
data[offset + 7] = (i >> 24) & 0xFF;
chunk.copyTo(new Uint8Array(data.buffer, offset + 12));
offset += 12 + chunk.byteLength;
}
const blob = new Blob([data.buffer], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = 'rendered_stream.ivf'; // Set the suggested filename
document.body.appendChild(a);
a.click();
} else {
console.error('Encoding config not supported');
}
});
document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => { document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => {
state.canvas.zoom_level = config.benchmark.zoom_level; state.canvas.zoom_level = config.benchmark.zoom_level;
state.canvas.offset.x = config.benchmark.offset.x; state.canvas.offset.x = config.benchmark.offset.x;
@ -92,7 +226,7 @@ function zenmode() {
} }
function enter_picker_mode(state, context) { function enter_picker_mode(state, context) {
if (state.tools.active === 'pencil') { // or other drawing tools if (['pencil', 'ruler'].includes(state.tools.active)) { // or other drawing tools
document.querySelector('canvas').classList.add('picker'); document.querySelector('canvas').classList.add('picker');
document.querySelector('.picker-preview-outer').classList.remove('dhide'); document.querySelector('.picker-preview-outer').classList.remove('dhide');
document.querySelector('.brush-dom').classList.add('dhide'); document.querySelector('.brush-dom').classList.add('dhide');
@ -170,7 +304,7 @@ function keydown(e, state, context) {
function keyup(e, state, context) { function keyup(e, state, context) {
if (config.debug_print) { if (config.debug_print) {
console.debug('keydown', e.code); console.debug('keyup', e.code);
} }
if (e.code === 'Space' && state.spacedown) { if (e.code === 'Space' && state.spacedown) {
@ -361,22 +495,16 @@ function pointermove(e, state, context) {
} }
if (state.colorpicking) { if (state.colorpicking) {
update_color_picker_color(state, context, canvasp); if (!e.ctrlKey) {
exit_picker_mode(state);
} else {
update_color_picker_color(state, context, canvasp);
}
} }
if (state.zooming) { if (state.zooming) {
const zooming_in = e.movementY > 0; const zoom_level_increment = Math.round(Math.sign(e.movementY) * Math.sqrt(Math.abs(e.movementY)));
const zooming_out = e.movementY < 0; let zoom_level = state.canvas.zoom_level + zoom_level_increment;
let zoom_level = null;
if (zooming_in) {
zoom_level = state.canvas.zoom_level + 1
} else if (zooming_out) {
zoom_level = state.canvas.zoom_level - 1;
} else {
return;
}
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) { if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) {
return; return;
@ -590,8 +718,11 @@ function pointerleave(e, state, context) {
context.canvas.classList.remove('movemode'); context.canvas.classList.remove('movemode');
} }
//exit_picker_mode(state); // exit_picker_mode(state);
// something else? }
function pointercancel(e, state, context) {
console.log('CANCEL');
} }
function update_cursor(state) { function update_cursor(state) {

16
client/webgl_shaders.js

@ -16,7 +16,7 @@ const sdf_vs_src = `#version 300 es
out vec3 v_color; out vec3 v_color;
flat out vec2 v_thickness; out float v_scalefade;
void main() { void main() {
vec2 screen02; vec2 screen02;
@ -31,7 +31,12 @@ const sdf_vs_src = `#version 300 es
int stroke_data_y = stroke_index / u_stroke_texture_size; int stroke_data_y = stroke_index / u_stroke_texture_size;
int stroke_data_x = stroke_index % u_stroke_texture_size; int stroke_data_x = stroke_index % u_stroke_texture_size;
uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0);
float radius = float(stroke_data.w); float canvas_pixel = 1.0 / u_scale.x; // assuming square pixels here..
float radius = max(0.5 * canvas_pixel, float(stroke_data.w));
// Fade from 1.0 to 0.0 based on sqrt distance beyond being 1 pixel wide? (compeltely ad-hoc, picked whatever looked good to me)
float scalefade = float(stroke_data.w) / canvas_pixel;
v_scalefade = min(1.0, sqrt(scalefade));
vec2 pos; vec2 pos;
@ -80,12 +85,13 @@ const sdf_fs_src = `#version 300 es
uniform float u_opacity_multipliter; uniform float u_opacity_multipliter;
in vec3 v_color; in vec3 v_color;
in float v_scalefade;
layout(location = 0) out vec4 FragColor; layout(location = 0) out vec4 FragColor;
void main() { void main() {
if (u_debug_mode == 0) { if (u_debug_mode == 0) {
float alpha = 0.75 * u_opacity_multipliter; float alpha = u_opacity_multipliter * v_scalefade;
FragColor = vec4(v_color * alpha, alpha); FragColor = vec4(v_color * alpha, alpha);
} else { } else {
FragColor = vec4(u_debug_color, 0.8); FragColor = vec4(u_debug_color, 0.8);
@ -382,12 +388,12 @@ function init_webgl(state, context) {
gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']); 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_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_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.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));
gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); 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_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_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 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));
const resize_canvas = (entries) => { const resize_canvas = (entries) => {
// https://www.khronos.org/webgl/wiki/HandlingHighDPI // https://www.khronos.org/webgl/wiki/HandlingHighDPI

Loading…
Cancel
Save