Browse Source

Actually perfect smoothing. Amazing, good, Demetri.y

sdf
Aleksey Olokhtonov 2 days ago
parent
commit
cf4b1880d2
  1. 8
      client/config.js
  2. 84
      client/math.js
  3. 85
      client/webgl_geometry.js

8
client/config.js

@ -33,7 +33,13 @@ const config = { @@ -33,7 +33,13 @@ const config = {
},
debug_force_lod: null,
demetri_ms: 40,
p: 'demetri',
p: 'avg',
avg_window: 10,
// WR is the weight ratio of the most recent point vs the oldest point used
// to fit, linearly interpolated for the rest; 1.0 means even weighting of all
// data, 5.0 means fitting the most recent point matters 5x in the optimization
wr: 1.0,
/*
* points of interest (desk/zoomlevel/x/y)

84
client/math.js

@ -297,6 +297,16 @@ function dotn(a, b) { @@ -297,6 +297,16 @@ function dotn(a, b) {
return r;
}
function dotn3(a, b, c) {
let r = 0;
for (let i = 0; i < a.length; ++i) {
r += a[i] * b[i] * c[i];
}
return r;
}
function mix(a, b, t) {
return a * t + b * (1 - t);
}
@ -478,9 +488,75 @@ function stroke_intersects_capsule(state, stroke, a, b, radius) { @@ -478,9 +488,75 @@ function stroke_intersects_capsule(state, stroke, a, b, radius) {
return false;
}
function estimate_next_point(ts, xs, ys, dt=0) {
function proj_remove_x_from_y(xs, ys, ws) {
const dxx = dotn3(xs, xs, ws);
const dxy = dotn3(xs, ys, ws);
if (dxx < 1e-6) {
return [0, ys];
}
const c = dxy / dxx;
const res = [];
for (let i = 0; i < xs.length; ++i) {
res.push(ys[i] - c * xs[i]);
}
return [c, res];
}
function estimate_next_point2(ts, xs, ys, wr=1.0) {
const N = ts.length;
if (N < 2) {
return [xs[N - 1], ys[N - 1]];
}
// inner product weight, power-law ramp over time from W0 to 1.0 (max)
const t0 = ts[0];
const t1 = ts[N - 1];
const ws = [];
// constant and quadratic basis terms
const q0 = [];
const ss = [];
for (const t of ts) {
ws.push(1 + ((wr - 1) * (t - t0) / (t1 - t0)));
q0.push(1);
ss.push(t * t);
}
// constant term
const [c0x, xs_0] = proj_remove_x_from_y(q0, xs, ws);
const [c0y, ys_0] = proj_remove_x_from_y(q0, ys, ws);
const [c0t, ts_0] = proj_remove_x_from_y(q0, ts, ws);
const [c0s, ss_0] = proj_remove_x_from_y(q0, ss, ws);
// linear term
const [c01x, xs_01] = proj_remove_x_from_y(ts_0, xs_0, ws);
const [c01y, ys_01] = proj_remove_x_from_y(ts_0, ys_0, ws);
// don't need to do ts here because it's guaranteed to go to zero
const [c01s, ss_01] = proj_remove_x_from_y(ts_0, ss_0, ws);
// quadratic term
const [c012x, xs_012] = proj_remove_x_from_y(ss_01, xs_0, ws);
const [c012y, ys_012] = proj_remove_x_from_y(ss_01, ys_0, ws);
const reconstructed_x = c0x * 1 + c01x * ts_0[N - 1] + c012x * ss_01[N - 1];
const reconstructed_y = c0y * 1 + c01y * ts_0[N - 1] + c012y * ss_01[N - 1];
return [reconstructed_x, reconstructed_y];
}
function estimate_next_point1(ts, xs, ys, dt=0) {
const N = ts.length;
if (N < 2) {
return [xs[N - 1], ys[N - 1]];
}
// mean values
let mx = 0, my = 0, mt = 0;
@ -494,10 +570,6 @@ function estimate_next_point(ts, xs, ys, dt=0) { @@ -494,10 +570,6 @@ function estimate_next_point(ts, xs, ys, dt=0) {
mx /= N;
my /= N;
if (N < 2) {
return [xs[N - 1], ys[N - 1]];
}
// orthogonalize against constant term
for (let i = 0; i < N; ++i) {
ts[i] -= mt;
@ -548,7 +620,7 @@ function smooth_curve(points, window_ms=config.demetri_ms, dt=0) { @@ -548,7 +620,7 @@ function smooth_curve(points, window_ms=config.demetri_ms, dt=0) {
y_window.push(p.y);
}
const [nx, ny] = estimate_next_point(t_window, x_window, y_window, dt);
const [nx, ny] = estimate_next_point2(t_window, x_window, y_window, config.wr);
result.push({
'x': nx,

85
client/webgl_geometry.js

@ -252,50 +252,87 @@ function geometry_add_prepoint_raw(state, context, player_id, point, is_pen, raw @@ -252,50 +252,87 @@ function geometry_add_prepoint_raw(state, context, player_id, point, is_pen, raw
recompute_dynamic_data(state, context);
}
function average_screen_speed(state, points, last, range=10) {
let screen_last = canvas_to_screen(state, points[points.length - 1]);
let sum_speed = 0;
let n = 0;
for (let i = points.length - 1; i >= 0 && i >= points.length - range; --i) {
const point = points[i];
const screen_this = canvas_to_screen(state, point);
const screen_dx = Math.abs(screen_this.x - screen_last.x);
const screen_dy = Math.abs(screen_this.y - screen_last.y);
const screen_speed = Math.sqrt(screen_dx * screen_dx + screen_dy * screen_dy);
sum_speed += screen_speed;
n++;
screen_last = screen_this;
}
if (n === 0) {
return 0;
}
return sum_speed / n;
}
function geometry_add_prepoint_new(state, context, player_id, point, is_pen, raw = false) {
if (!state.online) return;
const player = state.players[player_id];
const stroke = player.strokes[player.strokes.length - 1];
const points = stroke.points;
const raw_points = stroke.raw_points;
if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
}
const exp_window = 3;
if (points.length > exp_window) {
let xsum = 0;
let ysum = 0;
let avg_window = 0;
const screen_last = canvas_to_screen(state, points[points.length - 1]);
const screen_this = canvas_to_screen(state, point);
if (raw_points.length > 0) {
const screen_speed = average_screen_speed(state, raw_points, point);
const screen_dx = screen_this.x - screen_last.x;
const screen_dy = screen_this.y - screen_last.y;
// Empirically chosen.
// TODO: dpi scaling?
const bot = 1;
const top = 10;
// TODO: Higher (screen space!) speed gives more weight to the new point
const weight_x = 1;
const weight_y = 1;
const max_avg_window = config.avg_window;
for (let i = points.length - exp_window; i < points.length; ++i) {
xsum += points[i].x * weight_x;
ysum += points[i].y * weight_y;
if (screen_speed <= bot) {
avg_window = max_avg_window;
} else if (screen_speed >= top) {
avg_window = 0;
} else {
const onezero = 1.0 - (screen_speed - bot) / (top - bot);
avg_window = Math.floor(onezero * max_avg_window);
}
}
xsum += point.x * weight_x;
ysum += point.y * weight_y
avg_window = Math.min(avg_window, raw_points.length);
points.push({
'x': xsum / (exp_window + 1),
'y': ysum / (exp_window + 1),
'pressure': point.pressure
});
} else {
points.push(point);
let xsum = 0;
let ysum = 0;
for (let i = raw_points.length - avg_window; i < raw_points.length; ++i) {
xsum += raw_points[i].x;
ysum += raw_points[i].y;
}
xsum += point.x;
ysum += point.y;
points.push({
'x': xsum / (avg_window + 1),
'y': ysum / (avg_window + 1),
'pressure': point.pressure
});
stroke.raw_points.push(point);
recompute_dynamic_data(state, context);
}

Loading…
Cancel
Save