diff --git a/client/config.js b/client/config.js
index 9732f52..ed71c32 100644
--- a/client/config.js
+++ b/client/config.js
@@ -6,7 +6,8 @@ const config = {
ws_reconnect_timeout: 2000,
brush_preview_timeout: 1000,
second_finger_timeout: 500,
- animation_decay: 16,
+ // animation_decay: 16,
+ animation_decay: 10,
vertical_zoom_speed_multiplier: 3,
debug_print: false,
draw_bvh: false,
diff --git a/client/index.html b/client/index.html
index d750018..ab01c9b 100644
--- a/client/index.html
+++ b/client/index.html
@@ -47,6 +47,7 @@
+
@@ -151,7 +152,6 @@
-
diff --git a/client/webgl_draw.js b/client/webgl_draw.js
index cf69519..cd79be5 100644
--- a/client/webgl_draw.js
+++ b/client/webgl_draw.js
@@ -235,7 +235,7 @@ function draw_strokes(state, width, height, programs, gl, lod_levels, segment_co
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 cpu_before = performance.now();
@@ -631,7 +631,9 @@ async function draw(state, context, animate, ts) {
if (state.canvas.target_zoom != state.canvas.zoom) {
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);
+ }
}
}
diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js
index d678164..5c02115 100644
--- a/client/webgl_listeners.js
+++ b/client/webgl_listeners.js
@@ -41,6 +41,178 @@ function debug_panel_init(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 render_output_framerate = 60;
+ const start_movement_frame = 240;
+ const start_movement2_frame = 480;
+ let updated_zoom = false;
+
+ if (render_framerate % render_output_framerate !== 0) {
+ console.error('Render framerate MUST be divisible by output framerate');
+ return;
+ }
+
+ const render_ratio = render_framerate / render_output_framerate;
+
+ if (render_frames % render_ratio !== 0) {
+ console.error('Render frames MUST be divisible render ratio');
+ return;
+ }
+
+ const render_output_frames = render_frames / render_ratio;
+
+ const encoder_config = {
+ codec: "vp8",
+ width: canvas.width,
+ height: canvas.height,
+ displayWidth: canvas.width,
+ displayHeight: canvas.height,
+ bitrate: 100_000_000,
+ framerate: render_output_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))
+
+ const dst = new Uint8Array(canvas.width * canvas.height * 4);
+ const accum = new Uint16Array(canvas.width * canvas.height * 4); // double the size to fit many samples
+ const accum_final = new Uint8Array(canvas.width * canvas.height * 4);
+
+ for (let i = 0; i < render_output_frames; ++i) {
+ const time_now = i / render_output_framerate;
+
+ accum.fill(0);
+
+ for (let subframe = 0; subframe < render_ratio; ++subframe) {
+ await draw(state, context, i > 0 || subframe > 0, (time_now + (subframe / render_ratio) / render_output_framerate) * 1000, true);
+ context.gl.readPixels(0, 0, canvas.width, canvas.height, context.gl.RGBA, context.gl.UNSIGNED_BYTE, dst);
+
+ for (let j = 0; j < dst.byteLength; ++j) {
+ accum[j] += dst[j];
+ }
+
+ if (start_movement_frame <= i * render_ratio && i * render_ratio < 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 * render_ratio > 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;
+ }
+ }
+
+ for (let y = 0; y < canvas.height; ++y) {
+ for (let x = 0; x < canvas.width; ++x) {
+ for (let c = 0; c < 4; ++c) {
+ accum_final[(canvas.height - 1 - y) * canvas.width * 4 + x * 4 + c] = Math.round(accum[y * canvas.width * 4 + x * 4 + c] / render_ratio);
+ }
+ }
+ }
+
+ const frame = new VideoFrame(accum_final, { timestamp: time_now, codedWidth: canvas.width, codedHeight: canvas.height, format: 'RGBA'});
+ encoder.encode(frame, {keyFrame: true});
+ frame.close();
+ await encoder.flush();
+ }
+
+ 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_output_framerate; // timebase denom
+
+ data[20] = 1; // timebase numenator..?
+
+ data[24] = render_output_frames; // frame count
+
+ let offset = 32;
+ for (let i = 0; i < render_output_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) => {
state.canvas.zoom_level = config.benchmark.zoom_level;
state.canvas.offset.x = config.benchmark.offset.x;