Files
system-design/software-copyright/11-writech-sdk/core/stroke_smoother.c
T
2026-03-22 15:24:40 +08:00

345 lines
11 KiB
C

/**
* 自然写互动课堂应用开发SDK软件 V1.0
* 笔迹平滑算法核心模块 - 笔迹坐标平滑与笔锋渲染
*
* 跨平台C语言核心库
* 提供贝塞尔曲线平滑、笔锋宽度计算、坐标插值等算法
* 确保各平台SDK输出一致的笔迹渲染效果
*/
#ifndef STROKE_SMOOTHER_H
#define STROKE_SMOOTHER_H
#include <stdint.h>
#include <stddef.h>
#include <math.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ==================== 常量定义 ==================== */
#define MAX_SMOOTH_POINTS 4096 /* 平滑输出点缓冲区大小 */
#define MIN_POINT_DISTANCE 0.5f /* 最小点间距(低于此值合并) */
#define BEZIER_SEGMENTS 8 /* 贝塞尔曲线分段数 */
#define PRESSURE_SMOOTH_FACTOR 0.3f /* 压力平滑因子 */
/* ==================== 数据结构 ==================== */
/** 二维浮点坐标点 */
typedef struct {
float x;
float y;
} Vec2f;
/** 带压力和时间戳的笔迹点 */
typedef struct {
float x;
float y;
float pressure; /* 0.0-1.0 */
float width; /* 计算后的笔画宽度 */
uint32_t timestamp; /* 时间戳 */
} SmoothPoint;
/** 笔迹平滑器上下文 */
typedef struct {
/* 输入点缓冲区(最近4个点,用于三次贝塞尔) */
SmoothPoint input_buffer[4];
int buffer_count;
/* 输出点缓冲区 */
SmoothPoint output_buffer[MAX_SMOOTH_POINTS];
int output_count;
/* 笔画宽度配置 */
float min_width; /* 最小笔画宽度 */
float max_width; /* 最大笔画宽度 */
float velocity_scale; /* 速度对宽度的影响系数 */
/* 上一点的平滑压力值 */
float last_smooth_pressure;
/* 统计信息 */
uint32_t total_input_points;
uint32_t total_output_points;
} StrokeSmoother;
/* ==================== 数学工具函数 ==================== */
/** 两点间欧氏距离 */
static inline float vec2f_distance(Vec2f a, Vec2f b) {
float dx = b.x - a.x;
float dy = b.y - a.y;
return sqrtf(dx * dx + dy * dy);
}
/** 两点间线性插值 */
static inline Vec2f vec2f_lerp(Vec2f a, Vec2f b, float t) {
Vec2f result;
result.x = a.x + (b.x - a.x) * t;
result.y = a.y + (b.y - a.y) * t;
return result;
}
/** 浮点数线性插值 */
static inline float float_lerp(float a, float b, float t) {
return a + (b - a) * t;
}
/** 将值裁剪到范围 [min_val, max_val] */
static inline float float_clamp(float value, float min_val, float max_val) {
if (value < min_val) return min_val;
if (value > max_val) return max_val;
return value;
}
/* ==================== 贝塞尔曲线算法 ==================== */
/**
* 计算三次贝塞尔曲线上的点
* B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
*
* 用于平滑连接相邻坐标点,消除折角使笔画圆润
*/
static Vec2f cubic_bezier(Vec2f p0, Vec2f p1, Vec2f p2, Vec2f p3, float t) {
float u = 1.0f - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
Vec2f point;
point.x = uuu * p0.x + 3.0f * uu * t * p1.x + 3.0f * u * tt * p2.x + ttt * p3.x;
point.y = uuu * p0.y + 3.0f * uu * t * p1.y + 3.0f * u * tt * p2.y + ttt * p3.y;
return point;
}
/**
* 使用Catmull-Rom样条生成贝塞尔控制点
* 从4个数据点(p0,p1,p2,p3)计算p1到p2之间的贝塞尔控制点
* 确保曲线经过原始数据点(C1连续)
*/
static void catmull_rom_to_bezier(
Vec2f p0, Vec2f p1, Vec2f p2, Vec2f p3,
Vec2f* cp1_out, Vec2f* cp2_out
) {
float tension = 0.5f; /* 张力系数,0.5为标准Catmull-Rom */
cp1_out->x = p1.x + (p2.x - p0.x) * tension / 3.0f;
cp1_out->y = p1.y + (p2.y - p0.y) * tension / 3.0f;
cp2_out->x = p2.x - (p3.x - p1.x) * tension / 3.0f;
cp2_out->y = p2.y - (p3.y - p1.y) * tension / 3.0f;
}
/* ==================== 笔画宽度计算 ==================== */
/**
* 根据压力和速度计算笔画宽度
* 模拟真实毛笔/钢笔的笔锋效果:
* - 压力越大,笔画越粗
* - 速度越快,笔画越细(模拟快写时的飞白效果)
* - 起笔/收笔处渐变细化
*/
static float calculate_stroke_width(
float pressure, float velocity,
float min_width, float max_width, float velocity_scale
) {
/* 压力影响:压力0→最细,压力1→最粗 */
float pressure_width = min_width + (max_width - min_width) * pressure;
/* 速度衰减:速度快时笔画变细 */
float velocity_factor = 1.0f / (1.0f + velocity * velocity_scale);
float width = pressure_width * velocity_factor;
return float_clamp(width, min_width, max_width);
}
/* ==================== 笔迹平滑器API ==================== */
/**
* 初始化笔迹平滑器
*/
static void smoother_init(StrokeSmoother* ctx, float min_width, float max_width) {
ctx->buffer_count = 0;
ctx->output_count = 0;
ctx->min_width = min_width;
ctx->max_width = max_width;
ctx->velocity_scale = 0.005f;
ctx->last_smooth_pressure = 0.5f;
ctx->total_input_points = 0;
ctx->total_output_points = 0;
}
/**
* 输入一个新的坐标点
* 当缓冲区积累到4个点时,自动生成贝塞尔曲线平滑点
* 返回新生成的平滑点数量
*/
static int smoother_add_point(StrokeSmoother* ctx, float x, float y,
float pressure, uint32_t timestamp) {
ctx->total_input_points++;
/* 压力平滑(低通滤波器,避免压力值跳变) */
float smooth_pressure = ctx->last_smooth_pressure +
PRESSURE_SMOOTH_FACTOR * (pressure - ctx->last_smooth_pressure);
ctx->last_smooth_pressure = smooth_pressure;
/* 添加到输入缓冲区 */
int idx = ctx->buffer_count;
if (idx >= 4) {
/* 缓冲区满,移位 */
ctx->input_buffer[0] = ctx->input_buffer[1];
ctx->input_buffer[1] = ctx->input_buffer[2];
ctx->input_buffer[2] = ctx->input_buffer[3];
idx = 3;
}
ctx->input_buffer[idx].x = x;
ctx->input_buffer[idx].y = y;
ctx->input_buffer[idx].pressure = smooth_pressure;
ctx->input_buffer[idx].timestamp = timestamp;
ctx->buffer_count = idx + 1;
/* 不足4个点时直接输出原始点 */
if (ctx->buffer_count < 4) {
if (ctx->output_count < MAX_SMOOTH_POINTS) {
/* 计算速度和宽度 */
float velocity = 0;
if (ctx->buffer_count >= 2) {
Vec2f prev = {ctx->input_buffer[ctx->buffer_count-2].x, ctx->input_buffer[ctx->buffer_count-2].y};
Vec2f curr = {x, y};
float dt = (float)(timestamp - ctx->input_buffer[ctx->buffer_count-2].timestamp);
if (dt > 0) velocity = vec2f_distance(prev, curr) / dt * 1000.0f;
}
float width = calculate_stroke_width(smooth_pressure, velocity,
ctx->min_width, ctx->max_width, ctx->velocity_scale);
SmoothPoint sp = {x, y, smooth_pressure, width, timestamp};
ctx->output_buffer[ctx->output_count++] = sp;
ctx->total_output_points++;
return 1;
}
return 0;
}
/* 4个点准备好,生成贝塞尔曲线 */
Vec2f p0 = {ctx->input_buffer[0].x, ctx->input_buffer[0].y};
Vec2f p1 = {ctx->input_buffer[1].x, ctx->input_buffer[1].y};
Vec2f p2 = {ctx->input_buffer[2].x, ctx->input_buffer[2].y};
Vec2f p3 = {ctx->input_buffer[3].x, ctx->input_buffer[3].y};
/* 计算贝塞尔控制点 */
Vec2f cp1, cp2;
catmull_rom_to_bezier(p0, p1, p2, p3, &cp1, &cp2);
/* 在p1到p2之间生成平滑点 */
int new_points = 0;
for (int i = 0; i <= BEZIER_SEGMENTS; i++) {
if (ctx->output_count >= MAX_SMOOTH_POINTS) break;
float t = (float)i / BEZIER_SEGMENTS;
Vec2f pt = cubic_bezier(p1, cp1, cp2, p2, t);
/* 插值压力和时间戳 */
float interp_pressure = float_lerp(ctx->input_buffer[1].pressure,
ctx->input_buffer[2].pressure, t);
uint32_t interp_time = (uint32_t)float_lerp(
(float)ctx->input_buffer[1].timestamp,
(float)ctx->input_buffer[2].timestamp, t);
/* 计算速度 */
float velocity = 0;
if (ctx->output_count > 0) {
SmoothPoint* prev = &ctx->output_buffer[ctx->output_count - 1];
Vec2f prev_v = {prev->x, prev->y};
float dt = (float)(interp_time - prev->timestamp);
if (dt > 0) velocity = vec2f_distance(prev_v, pt) / dt * 1000.0f;
}
/* 计算笔画宽度 */
float width = calculate_stroke_width(interp_pressure, velocity,
ctx->min_width, ctx->max_width, ctx->velocity_scale);
/* 距离过滤:跳过距上一点太近的点 */
if (ctx->output_count > 0) {
SmoothPoint* prev = &ctx->output_buffer[ctx->output_count - 1];
Vec2f prev_v = {prev->x, prev->y};
if (vec2f_distance(prev_v, pt) < MIN_POINT_DISTANCE) continue;
}
SmoothPoint sp = {pt.x, pt.y, interp_pressure, width, interp_time};
ctx->output_buffer[ctx->output_count++] = sp;
ctx->total_output_points++;
new_points++;
}
return new_points;
}
/**
* 结束当前笔画(抬笔时调用)
* 输出最后一段贝塞尔曲线的收尾点
*/
static int smoother_end_stroke(StrokeSmoother* ctx) {
int new_points = 0;
/* 输出缓冲区中剩余的点 */
if (ctx->buffer_count >= 2 && ctx->output_count < MAX_SMOOTH_POINTS) {
int last = ctx->buffer_count - 1;
float width = calculate_stroke_width(
ctx->input_buffer[last].pressure * 0.5f, 0, /* 收笔处宽度减半 */
ctx->min_width, ctx->max_width, ctx->velocity_scale);
SmoothPoint sp = {
ctx->input_buffer[last].x, ctx->input_buffer[last].y,
ctx->input_buffer[last].pressure, width,
ctx->input_buffer[last].timestamp
};
ctx->output_buffer[ctx->output_count++] = sp;
new_points++;
}
/* 重置输入缓冲区 */
ctx->buffer_count = 0;
ctx->last_smooth_pressure = 0.5f;
return new_points;
}
/**
* 获取平滑后的输出点
*/
static inline const SmoothPoint* smoother_get_output(const StrokeSmoother* ctx) {
return ctx->output_buffer;
}
/**
* 获取输出点数量
*/
static inline int smoother_get_output_count(const StrokeSmoother* ctx) {
return ctx->output_count;
}
/**
* 清除输出缓冲区
*/
static inline void smoother_clear_output(StrokeSmoother* ctx) {
ctx->output_count = 0;
}
/**
* 获取统计信息
*/
static inline void smoother_get_stats(const StrokeSmoother* ctx,
uint32_t* input_count, uint32_t* output_count) {
if (input_count) *input_count = ctx->total_input_points;
if (output_count) *output_count = ctx->total_output_points;
}
#ifdef __cplusplus
}
#endif
#endif /* STROKE_SMOOTHER_H */