/** * 自然写互动课堂应用开发SDK软件 V1.0 * 笔迹平滑算法核心模块 - 笔迹坐标平滑与笔锋渲染 * * 跨平台C语言核心库 * 提供贝塞尔曲线平滑、笔锋宽度计算、坐标插值等算法 * 确保各平台SDK输出一致的笔迹渲染效果 */ #ifndef STROKE_SMOOTHER_H #define STROKE_SMOOTHER_H #include #include #include #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 */