/************************************************************//** * * @file: graphics.c * @author: Martin Fouilleul * @date: 23/01/2023 * @revision: * *****************************************************************/ #define _USE_MATH_DEFINES //NOTE: necessary for MSVC #include #define STB_TRUETYPE_IMPLEMENTATION #include"stb_truetype.h" #define STB_IMAGE_IMPLEMENTATION #include"stb_image.h" #include"debug_log.h" #include"graphics_internal.h" #define LOG_SUBSYSTEM "Graphics" //------------------------------------------------------------------------ // canvas structs //------------------------------------------------------------------------ typedef enum { MG_PATH_MOVE, MG_PATH_LINE, MG_PATH_QUADRATIC, MG_PATH_CUBIC } mg_path_elt_type; typedef struct mg_path_elt { mg_path_elt_type type; vec2 p[3]; } mg_path_elt; typedef struct mg_path_descriptor { u32 startIndex; u32 count; vec2 startPoint; } mg_path_descriptor; typedef struct mg_attributes { f32 width; f32 tolerance; mg_color color; mg_joint_type joint; f32 maxJointExcursion; mg_cap_type cap; mg_font font; f32 fontSize; mg_image image; mp_rect srcRegion; mp_rect clip; } mg_attributes; typedef struct mg_rounded_rect { f32 x; f32 y; f32 w; f32 h; f32 r; } mg_rounded_rect; typedef enum { MG_CMD_CLEAR = 0, MG_CMD_FILL, MG_CMD_STROKE, MG_CMD_RECT_FILL, MG_CMD_RECT_STROKE, MG_CMD_ROUND_RECT_FILL, MG_CMD_ROUND_RECT_STROKE, MG_CMD_ELLIPSE_FILL, MG_CMD_ELLIPSE_STROKE, MG_CMD_JUMP, MG_CMD_CLIP_PUSH, MG_CMD_CLIP_POP, } mg_primitive_cmd; typedef struct mg_primitive { mg_primitive_cmd cmd; mg_attributes attributes; mg_mat2x3 transform; union { mg_path_descriptor path; mp_rect rect; mg_rounded_rect roundedRect; utf32 codePoint; u32 jump; mg_mat2x3 matrix; //TODO remove when we use transform everywhere }; } mg_primitive; typedef struct mg_glyph_map_entry { unicode_range range; u32 firstGlyphIndex; } mg_glyph_map_entry; typedef struct mg_glyph_data { bool exists; utf32 codePoint; mg_path_descriptor pathDescriptor; mg_text_extents extents; //... } mg_glyph_data; enum { MG_STREAM_MAX_COUNT = 128, MG_IMAGE_MAX_COUNT = 128 }; enum { MG_MATRIX_STACK_MAX_DEPTH = 64, MG_CLIP_STACK_MAX_DEPTH = 64, MG_MAX_PATH_ELEMENT_COUNT = 2<<20, MG_MAX_PRIMITIVE_COUNT = 8<<10 }; typedef struct mg_font_data { list_elt freeListElt; u32 generation; u32 rangeCount; u32 glyphCount; u32 outlineCount; mg_glyph_map_entry* glyphMap; mg_glyph_data* glyphs; mg_path_elt* outlines; f32 unitsPerEm; mg_font_extents extents; } mg_font_data; typedef struct mg_canvas_data mg_canvas_data; typedef struct mg_resource_slot { list_elt freeListElt; u32 generation; union { mg_surface_data* surface; mg_image_data* image; mg_canvas_data* canvas; mg_font_data* font; //... }; } mg_resource_slot; enum { MG_MAX_RESOURCE_SLOTS = 128, //... }; typedef struct mg_resource_pool { mg_resource_slot slots[MG_MAX_RESOURCE_SLOTS]; list_info freeList; u32 nextIndex; } mg_resource_pool; typedef struct mg_data { bool init; mg_resource_pool surfaces; mg_resource_pool canvases; mg_resource_pool fonts; //... } mg_data; typedef struct mg_canvas_data { list_elt freeListElt; u32 generation; u64 frameCounter; mg_attributes attributes; bool textFlip; mg_path_elt pathElements[MG_MAX_PATH_ELEMENT_COUNT]; mg_path_descriptor path; vec2 subPathStartPoint; vec2 subPathLastPoint; mg_mat2x3 matrixStack[MG_MATRIX_STACK_MAX_DEPTH]; u32 matrixStackSize; mp_rect clipStack[MG_CLIP_STACK_MAX_DEPTH]; u32 clipStackSize; u32 primitiveCount; mg_primitive primitives[MG_MAX_PRIMITIVE_COUNT]; mg_resource_pool imagePool; vec2 atlasPos; u32 atlasLineHeight; mg_image blankImage; //NOTE: these are used at render time mp_rect clip; mg_mat2x3 transform; mg_image currentImage; mp_rect currentSrcRegion; vec4 shapeExtents; u32 nextShapeIndex; u32 shapeFirstVertexIndex; u32 vertexCount; u32 indexCount; mg_canvas_backend* backend; } mg_canvas_data; static mg_data __mgData = {0}; void mg_init() { if(!__mgData.init) { __mgData.init = true; //... } } //------------------------------------------------------------------------ // handle pools procedures //------------------------------------------------------------------------ mg_resource_slot* mg_resource_slot_alloc(mg_resource_pool* pool) { if(!__mgData.init) { //TODO: see if we require an init at all, and if we want to make it explicit to the user mg_init(); } mg_resource_slot* slot = ListPopEntry(&pool->freeList, mg_resource_slot, freeListElt); if(!slot && pool->nextIndex < MG_MAX_RESOURCE_SLOTS) { slot = &pool->slots[pool->nextIndex]; slot->generation = 1; pool->nextIndex++; } return(slot); } void mg_resource_slot_recycle(mg_resource_pool* pool, mg_resource_slot* slot) { DEBUG_ASSERT(slot >= pool->slots && slot < pool->slots + MG_MAX_RESOURCE_SLOTS); #ifdef DEBUG if(slot->generation == UINT32_MAX) { LOG_ERROR("surface slot generation wrap around\n"); } #endif slot->generation++; ListPush(&pool->freeList, &slot->freeListElt); } mg_resource_slot* mg_resource_slot_from_handle(mg_resource_pool* pool, u64 h) { u32 index = h>>32; u32 generation = h & 0xffffffff; if(index >= MG_MAX_RESOURCE_SLOTS) { return(0); } mg_resource_slot* slot = &pool->slots[index]; if(slot->generation != generation) { return(0); } else { return(slot); } } u64 mg_resource_handle_from_slot(mg_resource_pool* pool, mg_resource_slot* slot) { DEBUG_ASSERT( (slot - pool->slots) >= 0 && (slot - pool->slots) < MG_MAX_RESOURCE_SLOTS); u64 h = ((u64)(slot - pool->slots))<<32 |((u64)(slot->generation)); return(h); } void mg_resource_handle_recycle(mg_resource_pool* pool, u64 h) { mg_resource_slot* slot = mg_resource_slot_from_handle(pool, h); if(slot) { mg_resource_slot_recycle(pool, slot); } } //------------------------------------------------------------------------ // surface handles //------------------------------------------------------------------------ mg_surface mg_surface_nil() { return((mg_surface){.h = 0}); } bool mg_surface_is_nil(mg_surface surface) { return(surface.h == 0); } mg_surface mg_surface_alloc_handle(mg_surface_data* surface) { mg_resource_slot* slot = mg_resource_slot_alloc(&__mgData.surfaces); if(!slot) { LOG_ERROR("no more surface slots\n"); return(mg_surface_nil()); } slot->surface = surface; u64 h = mg_resource_handle_from_slot(&__mgData.surfaces, slot); mg_surface handle = {h}; return(handle); } mg_surface_data* mg_surface_data_from_handle(mg_surface surface) { mg_resource_slot* slot = mg_resource_slot_from_handle(&__mgData.surfaces, surface.h); mg_surface_data* data = 0; if(slot) { data = slot->surface; } return(data); } //------------------------------------------------------------------------ // canvas handles //------------------------------------------------------------------------ mg_canvas mg_canvas_nil() { return((mg_canvas){.h = 0}); } bool mg_canvas_is_nil(mg_canvas canvas) { return(canvas.h == 0); } mg_canvas mg_canvas_alloc_handle(mg_canvas_data* canvas) { mg_resource_slot* slot = mg_resource_slot_alloc(&__mgData.canvases); if(!slot) { LOG_ERROR("no more canvas slots\n"); return(mg_canvas_nil()); } slot->canvas = canvas; u64 h = mg_resource_handle_from_slot(&__mgData.canvases, slot); mg_canvas handle = {h}; return(handle); } mg_canvas_data* mg_canvas_data_from_handle(mg_canvas canvas) { mg_canvas_data* data = 0; mg_resource_slot* slot = mg_resource_slot_from_handle(&__mgData.canvases, canvas.h); if(slot) { data = slot->canvas; } return(data); } //------------------------------------------------------------------------ // image handles //------------------------------------------------------------------------ mg_image mg_image_nil() { return((mg_image){.h = 0}); } bool mg_image_is_nil(mg_image image) { return(image.h == 0); } mg_image mg_image_alloc_handle(mg_canvas_data* canvas, mg_image_data* image) { mg_resource_slot* slot = mg_resource_slot_alloc(&canvas->imagePool); if(!slot) { LOG_ERROR("no more canvas slots\n"); return(mg_image_nil()); } slot->image = image; u64 h = mg_resource_handle_from_slot(&canvas->imagePool, slot); mg_image handle = {h}; return(handle); } mg_image_data* mg_image_data_from_handle(mg_canvas_data* canvas, mg_image handle) { mg_image_data* data = 0; mg_resource_slot* slot = mg_resource_slot_from_handle(&canvas->imagePool, handle.h); if(slot) { data = slot->image; } return(data); } //------------------------------------------------------------------------ // font handles //------------------------------------------------------------------------ mg_font mg_font_nil() { return((mg_font){.h = 0}); } bool mg_font_is_nil(mg_font font) { return(font.h == 0); } mg_font mg_font_alloc_handle(mg_font_data* font) { mg_resource_slot* slot = mg_resource_slot_alloc(&__mgData.fonts); if(!slot) { LOG_ERROR("no more font slots\n"); return(mg_font_nil()); } slot->font = font; u64 h = mg_resource_handle_from_slot(&__mgData.fonts, slot); mg_font handle = {h}; return(handle); } mg_font_data* mg_font_data_from_handle(mg_font font) { mg_font_data* data = 0; mg_resource_slot* slot = mg_resource_slot_from_handle(&__mgData.fonts, font.h); if(slot) { data = slot->font; } return(data); } //--------------------------------------------------------------- // surface API //--------------------------------------------------------------- #if MG_COMPILE_BACKEND_GL #if defined(OS_WIN64) #include"wgl_surface.h" #define gl_surface_create_for_window mg_wgl_surface_create_for_window #elif defined(OS_MACOS) /* #include"nsgl_surface.h" #define gl_surface_create_for_window nsgl_surface_create_for_window */ #endif #endif #if MG_COMPILE_BACKEND_GLES #include"egl_surface.h" #endif #if MG_COMPILE_BACKEND_METAL #include"mtl_surface.h" #endif bool mg_is_surface_backend_available(mg_backend_id backend) { bool result = false; switch(backend) { #if MG_COMPILE_BACKEND_METAL case MG_BACKEND_METAL: #endif #if MG_COMPILE_BACKEND_GL case MG_BACKEND_GL: #endif #if MG_COMPILE_BACKEND_GLES case MG_BACKEND_GLES: #endif result = true; break; default: break; } return(result); } bool mg_is_canvas_backend_available(mg_backend_id backend) { bool result = false; switch(backend) { #if MG_COMPILE_BACKEND_METAL case MG_BACKEND_METAL: #endif #if MG_COMPILE_BACKEND_GL && defined(OS_WIN64) case MG_BACKEND_GL: #endif result = true; break; default: break; } return(result); } mg_surface mg_surface_create_for_window(mp_window window, mg_backend_id backend) { mg_surface surface = mg_surface_nil(); switch(backend) { #if MG_COMPILE_BACKEND_GL case MG_BACKEND_GL: surface = gl_surface_create_for_window(window); break; #endif #if MG_COMPILE_BACKEND_GLES case MG_BACKEND_GLES: surface = mg_egl_surface_create_for_window(window); break; #endif #if MG_COMPILE_BACKEND_METAL case MG_BACKEND_METAL: surface = mg_mtl_surface_create_for_window(window); break; #endif default: break; } return(surface); } void mg_surface_destroy(mg_surface handle) { DEBUG_ASSERT(__mgData.init); mg_resource_slot* slot = mg_resource_slot_from_handle(&__mgData.surfaces, handle.h); if(slot) { slot->surface->destroy(slot->surface); mg_resource_slot_recycle(&__mgData.surfaces, slot); } } void mg_surface_prepare(mg_surface surface) { DEBUG_ASSERT(__mgData.init); mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { surfaceData->prepare(surfaceData); } } void mg_surface_present(mg_surface surface) { DEBUG_ASSERT(__mgData.init); mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { surfaceData->present(surfaceData); } } void mg_surface_swap_interval(mg_surface surface, int swap) { DEBUG_ASSERT(__mgData.init); mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { surfaceData->swapInterval(surfaceData, swap); } } vec2 mg_surface_contents_scaling(mg_surface surface) { DEBUG_ASSERT(__mgData.init); vec2 scaling = {1, 1}; mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { scaling = surfaceData->contentsScaling(surfaceData); } return(scaling); } void mg_surface_set_frame(mg_surface surface, mp_rect frame) { DEBUG_ASSERT(__mgData.init); mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { surfaceData->setFrame(surfaceData, frame); } } mp_rect mg_surface_get_frame(mg_surface surface) { DEBUG_ASSERT(__mgData.init); mp_rect res = {0}; mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { res = surfaceData->getFrame(surfaceData); } return(res); } void mg_surface_set_hidden(mg_surface surface, bool hidden) { DEBUG_ASSERT(__mgData.init); mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { surfaceData->setHidden(surfaceData, hidden); } } bool mg_surface_get_hidden(mg_surface surface) { DEBUG_ASSERT(__mgData.init); bool res = false; mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { res = surfaceData->getHidden(surfaceData); } return(res); } //------------------------------------------------------------------------------------------ //NOTE(martin): graphics canvas internal //------------------------------------------------------------------------------------------ mp_thread_local mg_canvas_data* __mgCurrentCanvas = 0; mp_thread_local mg_canvas __mgCurrentCanvasHandle = {0}; //TODO: move elsewhere? mg_mat2x3 mg_mat2x3_mul_m(mg_mat2x3 lhs, mg_mat2x3 rhs) { mg_mat2x3 res; res.m[0] = lhs.m[0]*rhs.m[0] + lhs.m[1]*rhs.m[3]; res.m[1] = lhs.m[0]*rhs.m[1] + lhs.m[1]*rhs.m[4]; res.m[2] = lhs.m[0]*rhs.m[2] + lhs.m[1]*rhs.m[5] + lhs.m[2]; res.m[3] = lhs.m[3]*rhs.m[0] + lhs.m[4]*rhs.m[3]; res.m[4] = lhs.m[3]*rhs.m[1] + lhs.m[4]*rhs.m[4]; res.m[5] = lhs.m[3]*rhs.m[2] + lhs.m[4]*rhs.m[5] + lhs.m[5]; return(res); } mg_mat2x3 mg_mat2x3_inv(mg_mat2x3 x) { mg_mat2x3 res; res.m[0] = x.m[4]/(x.m[0]*x.m[4] - x.m[1]*x.m[3]); res.m[1] = x.m[1]/(x.m[1]*x.m[3] - x.m[0]*x.m[4]); res.m[3] = x.m[3]/(x.m[1]*x.m[3] - x.m[0]*x.m[4]); res.m[4] = x.m[0]/(x.m[0]*x.m[4] - x.m[1]*x.m[3]); res.m[2] = -(x.m[2]*res.m[0] + x.m[5]*res.m[1]); res.m[5] = -(x.m[2]*res.m[3] + x.m[5]*res.m[4]); return(res); } vec2 mg_mat2x3_mul(mg_mat2x3 m, vec2 p) { f32 x = p.x*m.m[0] + p.y*m.m[1] + m.m[2]; f32 y = p.x*m.m[3] + p.y*m.m[4] + m.m[5]; return((vec2){x, y}); } mg_mat2x3 mg_matrix_stack_top(mg_canvas_data* canvas) { if(canvas->matrixStackSize == 0) { return((mg_mat2x3){1, 0, 0, 0, 1, 0}); } else { return(canvas->matrixStack[canvas->matrixStackSize-1]); } } void mg_matrix_stack_push(mg_canvas_data* canvas, mg_mat2x3 transform) { if(canvas->matrixStackSize >= MG_MATRIX_STACK_MAX_DEPTH) { LOG_ERROR("matrix stack overflow\n"); } else { canvas->matrixStack[canvas->matrixStackSize] = transform; canvas->matrixStackSize++; } } void mg_matrix_stack_pop(mg_canvas_data* canvas) { if(canvas->matrixStackSize == 0) { LOG_ERROR("matrix stack underflow\n"); } else { canvas->matrixStackSize--; mg_matrix_stack_top(canvas); } } void mg_push_command(mg_canvas_data* canvas, mg_primitive primitive) { //NOTE(martin): push primitive and updates current stream, eventually patching a pending jump. ASSERT(canvas->primitiveCount < MG_MAX_PRIMITIVE_COUNT); canvas->primitives[canvas->primitiveCount] = primitive; canvas->primitives[canvas->primitiveCount].transform = mg_matrix_stack_top(canvas); canvas->primitiveCount++; } void mg_new_path(mg_canvas_data* canvas) { canvas->path.startIndex += canvas->path.count; canvas->path.count = 0; canvas->subPathStartPoint = canvas->subPathLastPoint; canvas->path.startPoint = canvas->subPathStartPoint; } void mg_path_push_elements(mg_canvas_data* canvas, u32 count, mg_path_elt* elements) { ASSERT(canvas->path.count + canvas->path.startIndex + count <= MG_MAX_PATH_ELEMENT_COUNT); memcpy(canvas->pathElements + canvas->path.startIndex + canvas->path.count, elements, count*sizeof(mg_path_elt)); canvas->path.count += count; } void mg_path_push_element(mg_canvas_data* canvas, mg_path_elt elt) { mg_path_push_elements(canvas, 1, &elt); } /////////////////////////////////////// WIP ///////////////////////////////////////////////////////////////////////// void mg_reset_shape_index(mg_canvas_data* canvas) { canvas->nextShapeIndex = 0; canvas->shapeExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; } void mg_finalize_shape(mg_canvas_data* canvas) { if(canvas->nextShapeIndex) { //NOTE: set shape's uv transform for the _current_ shape vec2 texSize = mg_image_size(canvas->currentImage); mp_rect srcRegion = canvas->currentSrcRegion; mp_rect destRegion = {canvas->shapeExtents.x, canvas->shapeExtents.y, canvas->shapeExtents.z - canvas->shapeExtents.x, canvas->shapeExtents.w - canvas->shapeExtents.y}; mg_mat2x3 srcRegionToTexture = {1/texSize.x, 0, srcRegion.x/texSize.x, 0, 1/texSize.y, srcRegion.y/texSize.y}; mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, 0, srcRegion.h/destRegion.h, 0}; mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, 0, 1, -destRegion.y}; mg_mat2x3 screenToUser = mg_mat2x3_inv(canvas->transform); mg_mat2x3 uvTransform = srcRegionToTexture; uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); int index = canvas->nextShapeIndex-1; mg_vertex_layout* layout = &canvas->backend->vertexLayout; *(mg_mat2x3*)(layout->uvTransformBuffer + index*layout->uvTransformStride) = uvTransform; } } u32 mg_next_shape(mg_canvas_data* canvas, mg_attributes* attributes) { mg_finalize_shape(canvas); canvas->currentSrcRegion = attributes->srcRegion; mg_vertex_layout* layout = &canvas->backend->vertexLayout; int index = canvas->nextShapeIndex; canvas->nextShapeIndex++; canvas->shapeFirstVertexIndex = canvas->vertexCount; mp_rect clip = {canvas->clip.x, canvas->clip.y, canvas->clip.x + canvas->clip.w - 1, canvas->clip.y + canvas->clip.h - 1}; *(mp_rect*)(((char*)layout->clipBuffer) + index*layout->clipStride) = clip; *(mg_color*)(((char*)layout->colorBuffer) + index*layout->colorStride) = attributes->color; return(index); } //TODO(martin): rename with something more explicit u32 mg_vertices_base_index(mg_canvas_data* canvas) { return(canvas->vertexCount); } int* mg_reserve_indices(mg_canvas_data* canvas, u32 indexCount) { mg_vertex_layout* layout = &canvas->backend->vertexLayout; //TODO: do something here... ASSERT(canvas->indexCount + indexCount < layout->maxIndexCount); int* base = ((int*)layout->indexBuffer) + canvas->indexCount; canvas->indexCount += indexCount; return(base); } void mg_push_vertex_cubic(mg_canvas_data* canvas, vec2 pos, vec4 cubic) { canvas->shapeExtents.x = minimum(canvas->shapeExtents.x, pos.x); canvas->shapeExtents.y = minimum(canvas->shapeExtents.y, pos.y); canvas->shapeExtents.z = maximum(canvas->shapeExtents.z, pos.x); canvas->shapeExtents.w = maximum(canvas->shapeExtents.w, pos.y); vec2 screenPos = mg_mat2x3_mul(canvas->transform, pos); mg_vertex_layout* layout = &canvas->backend->vertexLayout; DEBUG_ASSERT(canvas->vertexCount < layout->maxVertexCount); DEBUG_ASSERT(canvas->nextShapeIndex > 0); int shapeIndex = maximum(0, canvas->nextShapeIndex-1); u32 index = canvas->vertexCount; canvas->vertexCount++; *(vec2*)(((char*)layout->posBuffer) + index*layout->posStride) = screenPos; *(vec4*)(((char*)layout->cubicBuffer) + index*layout->cubicStride) = cubic; *(u32*)(((char*)layout->shapeIndexBuffer) + index*layout->shapeIndexStride) = shapeIndex; } void mg_push_vertex(mg_canvas_data* canvas, vec2 pos) { mg_push_vertex_cubic(canvas, pos, (vec4){1, 1, 1, 1}); } //----------------------------------------------------------------------------------------------------------- // Path Filling //----------------------------------------------------------------------------------------------------------- //NOTE(martin): forward declarations void mg_render_fill_cubic(mg_canvas_data* canvas, vec2 p[4]); //NOTE(martin): quadratics filling void mg_render_fill_quadratic(mg_canvas_data* canvas, vec2 p[3]) { u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 3); mg_push_vertex_cubic(canvas, (vec2){p[0].x, p[0].y}, (vec4){0, 0, 0, 1}); mg_push_vertex_cubic(canvas, (vec2){p[1].x, p[1].y}, (vec4){0.5, 0, 0.5, 1}); mg_push_vertex_cubic(canvas, (vec2){p[2].x, p[2].y}, (vec4){1, 1, 1, 1}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; } //NOTE(martin): cubic filling void mg_split_and_fill_cubic(mg_canvas_data* canvas, vec2 p[4], f32 tSplit) { int subVertexCount = 0; int subIndexCount = 0; f32 OneMinusTSplit = 1-tSplit; vec2 q0 = {OneMinusTSplit*p[0].x + tSplit*p[1].x, OneMinusTSplit*p[0].y + tSplit*p[1].y}; vec2 q1 = {OneMinusTSplit*p[1].x + tSplit*p[2].x, OneMinusTSplit*p[1].y + tSplit*p[2].y}; vec2 q2 = {OneMinusTSplit*p[2].x + tSplit*p[3].x, OneMinusTSplit*p[2].y + tSplit*p[3].y}; vec2 r0 = {OneMinusTSplit*q0.x + tSplit*q1.x, OneMinusTSplit*q0.y + tSplit*q1.y}; vec2 r1 = {OneMinusTSplit*q1.x + tSplit*q2.x, OneMinusTSplit*q1.y + tSplit*q2.y}; vec2 split = {OneMinusTSplit*r0.x + tSplit*r1.x, OneMinusTSplit*r0.y + tSplit*r1.y};; vec2 subPointsLow[4] = {p[0], q0, r0, split}; vec2 subPointsHigh[4] = {split, r1, q2, p[3]}; //NOTE(martin): add base triangle u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 3); mg_push_vertex(canvas, (vec2){p[0].x, p[0].y}); mg_push_vertex(canvas, (vec2){split.x, split.y}); mg_push_vertex(canvas, (vec2){p[3].x, p[3].y}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; mg_render_fill_cubic(canvas, subPointsLow); mg_render_fill_cubic(canvas, subPointsHigh); } void mg_render_fill_cubic(mg_canvas_data* canvas, vec2 p[4]) { LOG_DEBUG("graphics render fill cubic\n"); vec4 testCoords[4]; /*NOTE(martin): first convert the control points to power basis, multiplying by M3 | 1 0 0 0| M3 = |-3 3 0 0| | 3 -6 3 0| |-1 3 -3 1| ie: c0 = p0 c1 = -3*p0 + 3*p1 c2 = 3*p0 - 6*p1 + 3*p2 c3 = -p0 + 3*p1 - 3*p2 + p3 */ f32 c1x = 3.0*p[1].x - 3.0*p[0].x; f32 c1y = 3.0*p[1].y - 3.0*p[0].y; f32 c2x = 3.0*p[0].x + 3.0*p[2].x - 6.0*p[1].x; f32 c2y = 3.0*p[0].y + 3.0*p[2].y - 6.0*p[1].y; f32 c3x = 3.0*p[1].x - 3.0*p[2].x + p[3].x - p[0].x; f32 c3y = 3.0*p[1].y - 3.0*p[2].y + p[3].y - p[0].y; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //TODO(martin): we should do the tex coords computations in f64 and scale them to avoid f32 precision/range glitches in shader //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// c1x /= 10; c1y /= 10; c2x /= 10; c2y /= 10; c3x /= 10; c3y /= 10; /*NOTE(martin): now, compute determinants d0, d1, d2, d3, which gives the coefficients of the inflection points polynomial: I(t, s) = d0*t^3 - 3*d1*t^2*s + 3*d2*t*s^2 - d3*s^3 The roots of this polynomial are the inflection points of the parametric curve, in homogeneous coordinates (ie we can have an inflection point at inifinity with s=0). |x3 y3 w3| |x3 y3 w3| |x3 y3 w3| |x2 y2 w2| d0 = det |x2 y2 w2| d1 = -det |x2 y2 w2| d2 = det |x1 y1 w1| d3 = -det |x1 y1 w1| |x1 y1 w1| |x0 y0 w0| |x0 y0 w0| |x0 y0 w0| In our case, the pi.w equal 1 (no point at infinity), so _in_the_power_basis_, w1 = w2 = w3 = 0 and w0 = 1 (which also means d0 = 0) */ f32 d1 = c3y*c2x - c3x*c2y; f32 d2 = c3x*c1y - c3y*c1x; f32 d3 = c2y*c1x - c2x*c1y; //NOTE(martin): compute the second factor of the discriminant discr(I) = d1^2*(3*d2^2 - 4*d3*d1) f32 discrFactor2 = 3.0*Square(d2) - 4.0*d3*d1; //NOTE(martin): each following case gives the number of roots, hence the category of the parametric curve if(fabs(d1) < 0.1 && fabs(d2) < 0.1 && d3 != 0) { //NOTE(martin): quadratic degenerate case LOG_DEBUG("quadratic curve\n"); //NOTE(martin): compute quadratic curve control point, which is at p0 + 1.5*(p1-p0) = 1.5*p1 - 0.5*p0 vec2 quadControlPoints[3] = { p[0], {1.5*p[1].x - 0.5*p[0].x, 1.5*p[1].y - 0.5*p[0].y}, p[3]}; mg_render_fill_quadratic(canvas, quadControlPoints); return; } else if( (discrFactor2 > 0 && d1 != 0) ||(discrFactor2 == 0 && d1 != 0)) { //NOTE(martin): serpentine curve or cusp with inflection at infinity // (these two cases are handled the same way). LOG_DEBUG("%s\n", (discrFactor2 > 0 && d1 != 0) ? "serpentine curve" : "cusp with inflection at infinity"); //NOTE(martin): compute the solutions (tl, sl), (tm, sm), and (tn, sn) of the inflection point equation f32 tl = d2 + sqrt(discrFactor2/3); f32 sl = 2*d1; f32 tm = d2 - sqrt(discrFactor2/3); f32 sm = sl; /*NOTE(martin): the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: | tl*tm tl^3 tm^3 1 | | -sm*tl - sl*tm -3sl*tl^2 -3*sm*tm^2 0 | | sl*sm 3*sl^2*tl 3*sm^2*tm 0 | | 0 -sl^3 -sm^3 0 | This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n which are assigned as a 4D texture coordinates to control points. | 1 0 0 0 | M3^(-1) = | 1 1/3 0 0 | | 1 2/3 1/3 0 | | 1 1 1 1 | */ testCoords[0].x = tl*tm; testCoords[0].y = Cube(tl); testCoords[0].z = Cube(tm); testCoords[1].x = tl*tm - (sm*tl + sl*tm)/3; testCoords[1].y = Cube(tl) - sl*Square(tl); testCoords[1].z = Cube(tm) - sm*Square(tm); testCoords[2].x = tl*tm - (sm*tl + sl*tm)*2/3 + sl*sm/3; testCoords[2].y = Cube(tl) - 2*sl*Square(tl) + Square(sl)*tl; testCoords[2].z = Cube(tm) - 2*sm*Square(tm) + Square(sm)*tm; testCoords[3].x = tl*tm - (sm*tl + sl*tm) + sl*sm; testCoords[3].y = Cube(tl) - 3*sl*Square(tl) + 3*Square(sl)*tl - Cube(sl); testCoords[3].z = Cube(tm) - 3*sm*Square(tm) + 3*Square(sm)*tm - Cube(sm); } else if(discrFactor2 < 0 && d1 != 0) { //NOTE(martin): loop curve LOG_DEBUG("loop curve\n"); f32 td = d2 + sqrt(-discrFactor2); f32 sd = 2*d1; f32 te = d2 - sqrt(-discrFactor2); f32 se = sd; //NOTE(martin): if one of the parameters (td/sd) or (te/se) is in the interval [0,1], the double point // is inside the control points convex hull and would cause a shading anomaly. If this is // the case, subdivide the curve at that point //TODO: study edge case where td/sd ~ 1 or 0 (which causes an infinite recursion in split and fill). // quick fix for now is adding a little slop in the check... if(sd != 0 && td/sd < 0.99 && td/sd > 0.01) { LOG_DEBUG("split curve at first double point\n"); mg_split_and_fill_cubic(canvas, p, td/sd); return; } if(se != 0 && te/se < 0.99 && te/se > 0.01) { LOG_DEBUG("split curve at second double point\n"); mg_split_and_fill_cubic(canvas, p, te/se); return; } /*NOTE(martin): the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: | td*te td^2*te td*te^2 1 | | -se*td - sd*te -se*td^2 - 2sd*te*td -sd*te^2 - 2*se*td*te 0 | | sd*se te*sd^2 + 2*se*td*sd td*se^2 + 2*sd*te*se 0 | | 0 -sd^2*se -sd*se^2 0 | This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n which are assigned as a 4D texture coordinates to control points. | 1 0 0 0 | M3^(-1) = | 1 1/3 0 0 | | 1 2/3 1/3 0 | | 1 1 1 1 | */ testCoords[0].x = td*te; testCoords[0].y = Square(td)*te; testCoords[0].z = td*Square(te); testCoords[1].x = td*te - (se*td + sd*te)/3.0; testCoords[1].y = Square(td)*te - (se*Square(td) + 2.*sd*te*td)/3.0; testCoords[1].z = td*Square(te) - (sd*Square(te) + 2*se*td*te)/3.0; testCoords[2].x = td*te - 2.0*(se*td + sd*te)/3.0 + sd*se/3.0; testCoords[2].y = Square(td)*te - 2.0*(se*Square(td) + 2.0*sd*te*td)/3.0 + (te*Square(sd) + 2.0*se*td*sd)/3.0; testCoords[2].z = td*Square(te) - 2.0*(sd*Square(te) + 2.0*se*td*te)/3.0 + (td*Square(se) + 2.0*sd*te*se)/3.0; testCoords[3].x = td*te - (se*td + sd*te) + sd*se; testCoords[3].y = Square(td)*te - (se*Square(td) + 2.0*sd*te*td) + (te*Square(sd) + 2.0*se*td*sd) - Square(sd)*se; testCoords[3].z = td*Square(te) - (sd*Square(te) + 2.0*se*td*te) + (td*Square(se) + 2.0*sd*te*se) - sd*Square(se); } else if(d1 == 0 && d2 != 0) { //NOTE(martin): cusp with cusp at infinity LOG_DEBUG("cusp at infinity curve\n"); f32 tl = d3; f32 sl = 3*d2; /*NOTE(martin): the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: | tl tl^3 1 1 | | -sl -3sl*tl^2 0 0 | | 0 3*sl^2*tl 0 0 | | 0 -sl^3 0 0 | This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n which are assigned as a 4D texture coordinates to control points. | 1 0 0 0 | M3^(-1) = | 1 1/3 0 0 | | 1 2/3 1/3 0 | | 1 1 1 1 | */ testCoords[0].x = tl; testCoords[0].y = Cube(tl); testCoords[0].z = 1; testCoords[1].x = tl - sl/3; testCoords[1].y = Cube(tl) - sl*Square(tl); testCoords[1].z = 1; testCoords[2].x = tl - sl*2/3; testCoords[2].y = Cube(tl) - 2*sl*Square(tl) + Square(sl)*tl; testCoords[2].z = 1; testCoords[3].x = tl - sl; testCoords[3].y = Cube(tl) - 3*sl*Square(tl) + 3*Square(sl)*tl - Cube(sl); testCoords[3].z = 1; } else if(d1 == 0 && d2 == 0 && d3 == 0) { //NOTE(martin): line or point degenerate case, ignored LOG_DEBUG("line or point curve (ignored)\n"); return; } else { //TODO(martin): handle error ? put some epsilon slack on the conditions ? LOG_DEBUG("none of the above...\n"); ASSERT(0, "not implemented yet !"); return; } //NOTE(martin): compute convex hull indices using Gift wrapping / Jarvis' march algorithm int convexHullIndices[4]; int leftMostPointIndex = 0; for(int i=0; i<4; i++) { if(p[i].x < p[leftMostPointIndex].x) { leftMostPointIndex = i; } } int currentPointIndex = leftMostPointIndex; int i=0; int convexHullCount = 0; do { convexHullIndices[i] = currentPointIndex; convexHullCount++; int bestGuessIndex = 0; for(int j=0; j<4; j++) { vec2 bestGuessEdge = {.x = p[bestGuessIndex].x - p[currentPointIndex].x, .y = p[bestGuessIndex].y - p[currentPointIndex].y}; vec2 nextGuessEdge = {.x = p[j].x - p[currentPointIndex].x, .y = p[j].y - p[currentPointIndex].y}; //NOTE(martin): if control point j is on the right of current edge, it is a best guess // (in case of colinearity we choose the point which is farthest from the current point) f32 crossProduct = bestGuessEdge.x*nextGuessEdge.y - bestGuessEdge.y*nextGuessEdge.x; if( bestGuessIndex == currentPointIndex || crossProduct < 0) { bestGuessIndex = j; } else if(crossProduct == 0) { //NOTE(martin): if vectors v1, v2 are colinear and distinct, and ||v1|| > ||v2||, // either abs(v1.x) > abs(v2.x) or abs(v1.y) > abs(v2.y) // so we don't actually need to compute their norm to select the greatest // (and if v1 and v2 are equal we don't have to update our best guess.) //TODO(martin): in case of colinearity we should rather select the edge that has the greatest dot product with last edge ?? if(fabs(nextGuessEdge.x) > fabs(bestGuessEdge.x) || fabs(nextGuessEdge.y) > fabs(bestGuessEdge.y)) { bestGuessIndex = j; } } } i++; currentPointIndex = bestGuessIndex; } while(currentPointIndex != leftMostPointIndex && i<4); //TODO: quick fix, maybe later cull degenerate hulls beforehand if(convexHullCount <= 2) { //NOTE(martin): if convex hull has only two point, we have a degenerate cubic that displays nothing. return; } //NOTE(martin): rearrange convex hull to put p0 first int startIndex = -1; int orderedHullIndices[4]; for(int i=0; i= 0) { orderedHullIndices[i-startIndex] = convexHullIndices[i]; } } for(int i=0; i 0 // ie the 4th coordinate s flips the inside/outside test. // We affect s such that control points p1 and p2 are always outside the covered area. if(convexHullCount <= 3) { //NOTE(martin): the convex hull is a triangle //NOTE(martin): when we degenerate from 4 control points to a 3 points convex hull, this means one of the // control points p1 or p2 could be inside the covered area. We want to compute the test for the // control point which belongs to the convex hull, as we know it should be outside the covered area. // // Since there are 3 points in the hull and p0 is the first, and p3 belongs to the hull, this means we // must select the point of the convex hull which is neither the first, nor p3. int testPointIndex = orderedHullIndices[1] == 3 ? orderedHullIndices[2] : orderedHullIndices[1]; int outsideTest = 1; if(Cube(testCoords[testPointIndex].x)-testCoords[testPointIndex].y*testCoords[testPointIndex].z < 0) { outsideTest = -1; } u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 3); for(int i=0; i<3; i++) { vec4 cubic = testCoords[orderedHullIndices[i]]; cubic.w = outsideTest; mg_push_vertex_cubic(canvas, p[orderedHullIndices[i]], cubic); indices[i] = baseIndex + i; } } else if(orderedHullIndices[2] == 3) { //NOTE(martin): p1 and p2 are not on the same side of (p0,p3). The outside test can be different for // the two triangles int outsideTest1 = 1; int outsideTest2 = 1; int testIndex = orderedHullIndices[1]; if(Cube(testCoords[testIndex].x)-testCoords[testIndex].y*testCoords[testIndex].z < 0) { outsideTest1 = -1; } testIndex = orderedHullIndices[3]; if(Cube(testCoords[testIndex].x)-testCoords[testIndex].y*testCoords[testIndex].z < 0) { outsideTest2 = -1; } u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_push_vertex_cubic(canvas, p[orderedHullIndices[0]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[0]]), outsideTest1}); mg_push_vertex_cubic(canvas, p[orderedHullIndices[1]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[1]]), outsideTest1}); mg_push_vertex_cubic(canvas, p[orderedHullIndices[2]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[2]]), outsideTest1}); mg_push_vertex_cubic(canvas, p[orderedHullIndices[0]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[0]]), outsideTest2}); mg_push_vertex_cubic(canvas, p[orderedHullIndices[2]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[2]]), outsideTest2}); mg_push_vertex_cubic(canvas, p[orderedHullIndices[3]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[3]]), outsideTest2}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 3; indices[4] = baseIndex + 4; indices[5] = baseIndex + 5; } else { //NOTE(martin): if p1 and p2 are on the same side of (p0,p3), the outside test is the same for both triangles int outsideTest = 1; if(Cube(testCoords[1].x)-testCoords[1].y*testCoords[1].z < 0) { outsideTest = -1; } u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); for(int i=0; i<4; i++) { mg_push_vertex_cubic(canvas, p[orderedHullIndices[i]], (vec4){vec4_expand_xyz(testCoords[orderedHullIndices[i]]), outsideTest}); } indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } } //NOTE(martin): global path fill void mg_render_fill(mg_canvas_data* canvas, mg_path_elt* elements, mg_path_descriptor* path) { u32 eltCount = path->count; vec2 startPoint = path->startPoint; vec2 endPoint = path->startPoint; vec2 currentPoint = path->startPoint; for(int eltIndex=0; eltIndexp[0], elt->p[1], elt->p[2]}; switch(elt->type) { case MG_PATH_MOVE: { startPoint = elt->p[0]; endPoint = elt->p[0]; currentPoint = endPoint; continue; } break; case MG_PATH_LINE: { endPoint = controlPoints[1]; } break; case MG_PATH_QUADRATIC: { mg_render_fill_quadratic(canvas, controlPoints); endPoint = controlPoints[2]; } break; case MG_PATH_CUBIC: { mg_render_fill_cubic(canvas, controlPoints); endPoint = controlPoints[3]; } break; } //NOTE(martin): now fill interior triangle u32 baseIndex = mg_vertices_base_index(canvas); int* indices = mg_reserve_indices(canvas, 3); mg_push_vertex(canvas, startPoint); mg_push_vertex(canvas, currentPoint); mg_push_vertex(canvas, endPoint); indices[0] = baseIndex; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; currentPoint = endPoint; } } //----------------------------------------------------------------------------------------------------------- // Path Stroking //----------------------------------------------------------------------------------------------------------- void mg_render_stroke_line(mg_canvas_data* canvas, vec2 p[2], mg_attributes* attributes) { //NOTE(martin): get normals multiplied by halfWidth f32 halfW = attributes->width/2; vec2 n0 = {p[0].y - p[1].y, p[1].x - p[0].x}; f32 norm0 = sqrt(n0.x*n0.x + n0.y*n0.y); n0.x *= halfW/norm0; n0.y *= halfW/norm0; u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_push_vertex(canvas, (vec2){p[0].x + n0.x, p[0].y + n0.y}); mg_push_vertex(canvas, (vec2){p[1].x + n0.x, p[1].y + n0.y}); mg_push_vertex(canvas, (vec2){p[1].x - n0.x, p[1].y - n0.y}); mg_push_vertex(canvas, (vec2){p[0].x - n0.x, p[0].y - n0.y}); indices[0] = baseIndex; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } void mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) { ////////////////////////////////////////////////////////////////////////////////////// //WARN: quick fix for coincident middle control points if(count == 4 && (fabs(p[1].x - p[2].x) < 0.01) && (fabs(p[1].y - p[2].y) < 0.01)) { vec2 hull3[3] = {p[0], p[1], p[3]}; vec2 result3[3]; mg_offset_hull(3, hull3, result3, offset); result[0] = result3[0]; result[1] = result3[1]; result[2] = result3[1]; result[3] = result3[2]; return; } /////////////////////////////////////////////////////////////////////////////////////: //TODO(martin): review edge cases (coincident points ? colinear points ? control points pointing outward end point?) //NOTE(martin): first offset control point is just the offset of first control point vec2 n = {p[0].y - p[1].y, p[1].x - p[0].x}; f32 norm = sqrt(n.x*n.x + n.y*n.y); n.x *= offset/norm; n.y *= offset/norm; result[0].x = p[0].x + n.x; result[0].y = p[0].y + n.y; //NOTE(martin): subsequent offset control points are the intersection of offset control lines for(int i=1; iwidth); mg_offset_hull(3, p, negativeOffsetHull, -0.5 * attributes->width); //NOTE(martin): the distance d between the offset curve and the path must be between w/2-tolerance and w/2+tolerance // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this: // // (w/2-tolerance)^2 < d^2 < (w/2+tolerance)^2 // // we compute the maximum overshoot outside these bounds and split the curve at the corresponding parameter //TODO: maybe refactor by using tolerance in the _check_, not in the computation of the overshoot f32 tolerance = minimum(attributes->tolerance, 0.5 * attributes->width); f32 d2LowBound = Square(0.5 * attributes->width - attributes->tolerance); f32 d2HighBound = Square(0.5 * attributes->width + attributes->tolerance); f32 maxOvershoot = 0; f32 maxOvershootParameter = 0; for(int i=0; i maxOvershoot) { maxOvershoot = overshoot; maxOvershootParameter = t; } } if(maxOvershoot > 0) { vec2 splitLeft[3]; vec2 splitRight[3]; mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); mg_render_stroke_quadratic(canvas, splitLeft, attributes); mg_render_stroke_quadratic(canvas, splitRight, attributes); } else { //NOTE(martin): push the actual fill commands for the offset contour mg_next_shape(canvas, attributes); mg_render_fill_quadratic(canvas, positiveOffsetHull); mg_render_fill_quadratic(canvas, negativeOffsetHull); //NOTE(martin): add base triangles u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_push_vertex(canvas, positiveOffsetHull[0]); mg_push_vertex(canvas, positiveOffsetHull[2]); mg_push_vertex(canvas, negativeOffsetHull[2]); mg_push_vertex(canvas, negativeOffsetHull[0]); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } #undef CHECK_SAMPLE_COUNT } vec2 mg_cubic_get_point(vec2 p[4], f32 t) { vec2 r; f32 oneMt = 1-t; f32 oneMt2 = Square(oneMt); f32 oneMt3 = oneMt2*oneMt; f32 t2 = Square(t); f32 t3 = t2*t; r.x = oneMt3*p[0].x + 3*oneMt2*t*p[1].x + 3*oneMt*t2*p[2].x + t3*p[3].x; r.y = oneMt3*p[0].y + 3*oneMt2*t*p[1].y + 3*oneMt*t2*p[2].y + t3*p[3].y; return(r); } void mg_cubic_split(vec2 p[4], f32 t, vec2 outLeft[4], vec2 outRight[4]) { //NOTE(martin): split bezier curve p at parameter t, using De Casteljau's algorithm // the q_n are the points along the hull's segments at parameter t // the r_n are the points along the (q_n, q_n+1) segments at parameter t // s is the split point. f32 oneMt = 1-t; vec2 q0 = {oneMt*p[0].x + t*p[1].x, oneMt*p[0].y + t*p[1].y}; vec2 q1 = {oneMt*p[1].x + t*p[2].x, oneMt*p[1].y + t*p[2].y}; vec2 q2 = {oneMt*p[2].x + t*p[3].x, oneMt*p[2].y + t*p[3].y}; vec2 r0 = {oneMt*q0.x + t*q1.x, oneMt*q0.y + t*q1.y}; vec2 r1 = {oneMt*q1.x + t*q2.x, oneMt*q1.y + t*q2.y}; vec2 s = {oneMt*r0.x + t*r1.x, oneMt*r0.y + t*r1.y};; outLeft[0] = p[0]; outLeft[1] = q0; outLeft[2] = r0; outLeft[3] = s; outRight[0] = s; outRight[1] = r1; outRight[2] = q2; outRight[3] = p[3]; } void mg_render_stroke_cubic(mg_canvas_data* canvas, vec2 p[4], mg_attributes* attributes) { #define CHECK_SAMPLE_COUNT 5 f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; vec2 positiveOffsetHull[4]; vec2 negativeOffsetHull[4]; mg_offset_hull(4, p, positiveOffsetHull, 0.5 * attributes->width); mg_offset_hull(4, p, negativeOffsetHull, -0.5 * attributes->width); //NOTE(martin): the distance d between the offset curve and the path must be between w/2-tolerance and w/2+tolerance // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this: // // (w/2-tolerance)^2 < d^2 < (w/2+tolerance)^2 // // we compute the maximum overshoot outside these bounds and split the curve at the corresponding parameter //TODO: maybe refactor by using tolerance in the _check_, not in the computation of the overshoot f32 tolerance = minimum(attributes->tolerance, 0.5 * attributes->width); f32 d2LowBound = Square(0.5 * attributes->width - attributes->tolerance); f32 d2HighBound = Square(0.5 * attributes->width + attributes->tolerance); f32 maxOvershoot = 0; f32 maxOvershootParameter = 0; for(int i=0; i maxOvershoot) { maxOvershoot = overshoot; maxOvershootParameter = t; } } if(maxOvershoot > 0) { vec2 splitLeft[4]; vec2 splitRight[4]; mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); mg_render_stroke_cubic(canvas, splitLeft, attributes); mg_render_stroke_cubic(canvas, splitRight, attributes); //TODO: render joint between the split curves } else { //NOTE(martin): push the actual fill commands for the offset contour mg_next_shape(canvas, attributes); mg_render_fill_cubic(canvas, positiveOffsetHull); mg_render_fill_cubic(canvas, negativeOffsetHull); //NOTE(martin): add base triangles u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_push_vertex(canvas, positiveOffsetHull[0]); mg_push_vertex(canvas, positiveOffsetHull[3]); mg_push_vertex(canvas, negativeOffsetHull[3]); mg_push_vertex(canvas, negativeOffsetHull[0]); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } #undef CHECK_SAMPLE_COUNT } void mg_stroke_cap(mg_canvas_data* canvas, vec2 p0, vec2 direction, mg_attributes* attributes) { //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point f32 dn = sqrt(Square(direction.x) + Square(direction.y)); f32 alpha = 0.5 * attributes->width/dn; vec2 n0 = {-alpha*direction.y, alpha*direction.x}; vec2 m0 = {alpha*direction.x, alpha*direction.y}; mg_next_shape(canvas, attributes); u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_push_vertex(canvas, (vec2){p0.x + n0.x, p0.y + n0.y}); mg_push_vertex(canvas, (vec2){p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}); mg_push_vertex(canvas, (vec2){p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}); mg_push_vertex(canvas, (vec2){p0.x - n0.x, p0.y - n0.y}); indices[0] = baseIndex; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } void mg_stroke_joint(mg_canvas_data* canvas, vec2 p0, vec2 t0, vec2 t1, mg_attributes* attributes) { //NOTE(martin): compute the normals at the joint point f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); vec2 n0 = {-t0.y, t0.x}; n0.x /= norm_t0; n0.y /= norm_t0; vec2 n1 = {-t1.y, t1.x}; n1.x /= norm_t1; n1.y /= norm_t1; //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. // we flip them to face outwards if needed f32 crossZ = n0.x*n1.y - n0.y*n1.x; if(crossZ > 0) { n0.x *= -1; n0.y *= -1; n1.x *= -1; n1.y *= -1; } mg_next_shape(canvas, attributes); //NOTE(martin): use the same code as hull offset to find mitter point... /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 then v = u * (2*offset / norm(u)^2) (this can be derived from writing the pythagoras theorems in the triangles of the joint) */ f32 halfW = 0.5 * attributes->width; vec2 u = {n0.x + n1.x, n0.y + n1.y}; f32 uNormSquare = u.x*u.x + u.y*u.y; f32 alpha = attributes->width / uNormSquare; vec2 v = {u.x * alpha, u.y * alpha}; f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); if( attributes->joint == MG_JOINT_MITER && excursionSquare <= Square(attributes->maxJointExcursion)) { vec2 mitterPoint = {p0.x + v.x, p0.y + v.y}; u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_push_vertex(canvas, p0); mg_push_vertex(canvas, (vec2){p0.x + n0.x*halfW, p0.y + n0.y*halfW}); mg_push_vertex(canvas, mitterPoint); mg_push_vertex(canvas, (vec2){p0.x + n1.x*halfW, p0.y + n1.y*halfW}); indices[0] = baseIndex; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } else { //NOTE(martin): add a bevel joint u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 3); mg_push_vertex(canvas, p0); mg_push_vertex(canvas, (vec2){p0.x + n0.x*halfW, p0.y + n0.y*halfW}); mg_push_vertex(canvas, (vec2){p0.x + n1.x*halfW, p0.y + n1.y*halfW}); DEBUG_ASSERT(!isnan(n0.x) && !isnan(n0.y) && !isnan(n1.x) && !isnan(n1.y)); indices[0] = baseIndex; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; } } void mg_render_stroke_element(mg_canvas_data* canvas, mg_path_elt* element, mg_attributes* attributes, vec2 currentPoint, vec2* startTangent, vec2* endTangent, vec2* endPoint) { vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; int endPointIndex = 0; mg_next_shape(canvas, attributes); switch(element->type) { case MG_PATH_LINE: mg_render_stroke_line(canvas, controlPoints, attributes); endPointIndex = 1; break; case MG_PATH_QUADRATIC: mg_render_stroke_quadratic(canvas, controlPoints, attributes); endPointIndex = 2; break; case MG_PATH_CUBIC: mg_render_stroke_cubic(canvas, controlPoints, attributes); endPointIndex = 3; break; case MG_PATH_MOVE: ASSERT(0, "should be unreachable"); break; } *startTangent = (vec2){.x = controlPoints[1].x - controlPoints[0].x, .y = controlPoints[1].y - controlPoints[0].y}; *endTangent = (vec2){controlPoints[endPointIndex].x - controlPoints[endPointIndex-1].x, controlPoints[endPointIndex].y - controlPoints[endPointIndex-1].y}; *endPoint = controlPoints[endPointIndex]; } u32 mg_render_stroke_subpath(mg_canvas_data* canvas, mg_path_elt* elements, mg_path_descriptor* path, mg_attributes* attributes, u32 startIndex, vec2 startPoint) { u32 eltCount = path->count; DEBUG_ASSERT(startIndex < eltCount); vec2 currentPoint = startPoint; vec2 endPoint = {0, 0}; vec2 previousEndTangent = {0, 0}; vec2 firstTangent = {0, 0}; vec2 startTangent = {0, 0}; vec2 endTangent = {0, 0}; //NOTE(martin): render first element and compute first tangent mg_render_stroke_element(canvas, elements + startIndex, attributes, currentPoint, &startTangent, &endTangent, &endPoint); firstTangent = startTangent; previousEndTangent = endTangent; currentPoint = endPoint; //NOTE(martin): render subsequent elements along with their joints u32 eltIndex = startIndex + 1; for(; eltIndexjoint != MG_JOINT_NONE) { mg_stroke_joint(canvas, currentPoint, previousEndTangent, startTangent, attributes); } previousEndTangent = endTangent; currentPoint = endPoint; } u32 subPathEltCount = eltIndex - (startIndex+1); //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint if( subPathEltCount > 1 && startPoint.x == endPoint.x && startPoint.y == endPoint.y) { if(attributes->joint != MG_JOINT_NONE) { //NOTE(martin): add a closing joint if the path is closed mg_stroke_joint(canvas, endPoint, endTangent, firstTangent, attributes); } } else if(attributes->cap == MG_CAP_SQUARE) { //NOTE(martin): add start and end cap mg_stroke_cap(canvas, startPoint, (vec2){-startTangent.x, -startTangent.y}, attributes); mg_stroke_cap(canvas, endPoint, startTangent, attributes); } return(eltIndex); } void mg_render_stroke(mg_canvas_data* canvas, mg_path_elt* elements, mg_path_descriptor* path, mg_attributes* attributes) { u32 eltCount = path->count; DEBUG_ASSERT(eltCount); vec2 startPoint = path->startPoint; u32 startIndex = 0; while(startIndex < eltCount) { //NOTE(martin): eliminate leading moves while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) { startPoint = elements[startIndex].p[0]; startIndex++; } if(startIndex < eltCount) { startIndex = mg_render_stroke_subpath(canvas, elements, path, attributes, startIndex, startPoint); } } } //----------------------------------------------------------------------------------------------------------- // Fast shapes primitives //----------------------------------------------------------------------------------------------------------- void mg_render_rectangle_fill(mg_canvas_data* canvas, mp_rect rect, mg_attributes* attributes) { u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); mg_next_shape(canvas, attributes); mg_push_vertex(canvas, (vec2){rect.x, rect.y}); mg_push_vertex(canvas, (vec2){rect.x + rect.w, rect.y}); mg_push_vertex(canvas, (vec2){rect.x + rect.w, rect.y + rect.h}); mg_push_vertex(canvas, (vec2){rect.x, rect.y + rect.h}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } void mg_render_rectangle_stroke(mg_canvas_data* canvas, mp_rect rect, mg_attributes* attributes) { //NOTE(martin): stroke a rectangle by fill two scaled rectangles with the same shapeIndex. u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 12); mg_next_shape(canvas, attributes); //NOTE(martin): limit stroke width to the minimum dimension of the rectangle f32 width = minimum(attributes->width, minimum(rect.w, rect.h)); f32 halfW = width/2; // outer points mg_push_vertex(canvas, (vec2){rect.x - halfW, rect.y - halfW}); mg_push_vertex(canvas, (vec2){rect.x + rect.w + halfW, rect.y - halfW}); mg_push_vertex(canvas, (vec2){rect.x + rect.w + halfW, rect.y + rect.h + halfW}); mg_push_vertex(canvas, (vec2){rect.x - halfW, rect.y + rect.h + halfW}); // innter points mg_push_vertex(canvas, (vec2){rect.x + halfW, rect.y + halfW}); mg_push_vertex(canvas, (vec2){rect.x + rect.w - halfW, rect.y + halfW}); mg_push_vertex(canvas, (vec2){rect.x + rect.w - halfW, rect.y + rect.h - halfW}); mg_push_vertex(canvas, (vec2){rect.x + halfW, rect.y + rect.h - halfW}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; indices[6] = baseIndex + 4; indices[7] = baseIndex + 5; indices[8] = baseIndex + 6; indices[9] = baseIndex + 4; indices[10] = baseIndex + 6; indices[11] = baseIndex + 7; } void mg_render_fill_arc_corner(mg_canvas_data* canvas, f32 x, f32 y, f32 rx, f32 ry) { //NOTE(martin): draw a precomputed arc corner, using a bezier approximation u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); f32 cx = rx*4*(sqrt(2)-1)/3; f32 cy = ry*4*(sqrt(2)-1)/3; mg_push_vertex_cubic(canvas, (vec2){x, y + ry}, (vec4){-3.76797, -9.76362, 5.47912, -1}); mg_push_vertex_cubic(canvas, (vec2){x, y + ry - cy}, (vec4){-4.19896, -9.45223, 7.534, -1}); mg_push_vertex_cubic(canvas, (vec2){x + rx - cx, y}, (vec4){-4.19896, -7.534, 9.45223, -1}); mg_push_vertex_cubic(canvas, (vec2){x + rx, y}, (vec4){-3.76797, -5.47912, 9.76362, -1}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; } void mg_render_rounded_rectangle_fill_path(mg_canvas_data* canvas, mg_rounded_rect rect) { //NOTE(martin): draw a rounded rectangle by drawing a normal rectangle and 4 corners, // approximating an arc by a precomputed bezier curve u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 18); //NOTE(martin): inner cutted corner rectangle mg_push_vertex(canvas, (vec2){rect.x + rect.r, rect.y}); mg_push_vertex(canvas, (vec2){rect.x + rect.w - rect.r, rect.y}); mg_push_vertex(canvas, (vec2){rect.x + rect.w, rect.y + rect.r}); mg_push_vertex(canvas, (vec2){rect.x + rect.w, rect.y + rect.h - rect.r}); mg_push_vertex(canvas, (vec2){rect.x + rect.w - rect.r, rect.y + rect.h}); mg_push_vertex(canvas, (vec2){rect.x + rect.r, rect.y + rect.h}); mg_push_vertex(canvas, (vec2){rect.x, rect.y + rect.h - rect.r}); mg_push_vertex(canvas, (vec2){rect.x, rect.y + rect.r}); static const i32 fanIndices[18] = { 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 6, 0, 6, 7 }; // inner fan for(int i=0; i<18; i++) { indices[i] = fanIndices[i] + baseIndex; } mg_render_fill_arc_corner(canvas, rect.x, rect.y, rect.r, rect.r); mg_render_fill_arc_corner(canvas, rect.x + rect.w, rect.y, -rect.r, rect.r); mg_render_fill_arc_corner(canvas, rect.x + rect.w, rect.y + rect.h, -rect.r, -rect.r); mg_render_fill_arc_corner(canvas, rect.x, rect.y + rect.h, rect.r, -rect.r); } void mg_render_rounded_rectangle_fill(mg_canvas_data* canvas, mg_rounded_rect rect, mg_attributes* attributes) { mg_next_shape(canvas, attributes); mg_render_rounded_rectangle_fill_path(canvas, rect); } void mg_render_rounded_rectangle_stroke(mg_canvas_data* canvas, mg_rounded_rect rect, mg_attributes* attributes) { //NOTE(martin): stroke rounded rectangle by filling two scaled rounded rectangles with the same shapeIndex f32 width = minimum(attributes->width, minimum(rect.w, rect.h)); f32 halfW = width/2; mg_rounded_rect inner = {rect.x + halfW, rect.y + halfW, rect.w - width, rect.h - width, rect.r - halfW}; mg_rounded_rect outer = {rect.x - halfW, rect.y - halfW, rect.w + width, rect.h + width, rect.r + halfW}; mg_next_shape(canvas, attributes); mg_render_rounded_rectangle_fill_path(canvas, outer); mg_render_rounded_rectangle_fill_path(canvas, inner); } void mg_render_ellipse_fill_path(mg_canvas_data* canvas, mp_rect rect) { //NOTE(martin): draw a filled ellipse by drawing a diamond and 4 corners, // approximating an arc by a precomputed bezier curve f32 rx = rect.w/2; f32 ry = rect.h/2; u32 baseIndex = mg_vertices_base_index(canvas); i32* indices = mg_reserve_indices(canvas, 6); //NOTE(martin): inner diamond mg_push_vertex(canvas, (vec2){rect.x, rect.y + ry}); mg_push_vertex(canvas, (vec2){rect.x + rx, rect.y}); mg_push_vertex(canvas, (vec2){rect.x + rect.w, rect.y + ry}); mg_push_vertex(canvas, (vec2){rect.x + rx, rect.y + rect.h}); indices[0] = baseIndex + 0; indices[1] = baseIndex + 1; indices[2] = baseIndex + 2; indices[3] = baseIndex + 0; indices[4] = baseIndex + 2; indices[5] = baseIndex + 3; mg_render_fill_arc_corner(canvas, rect.x, rect.y, rx, ry); mg_render_fill_arc_corner(canvas, rect.x + rect.w, rect.y, -rx, ry); mg_render_fill_arc_corner(canvas, rect.x + rect.w, rect.y + rect.h, -rx, -ry); mg_render_fill_arc_corner(canvas, rect.x, rect.y + rect.h, rx, -ry); } void mg_render_ellipse_fill(mg_canvas_data* canvas, mp_rect rect, mg_attributes* attributes) { mg_next_shape(canvas, attributes); mg_render_ellipse_fill_path(canvas, rect); } void mg_render_ellipse_stroke(mg_canvas_data* canvas, mp_rect rect, mg_attributes* attributes) { //NOTE(martin): stroke by filling two scaled ellipsis with the same shapeIndex f32 width = minimum(attributes->width, minimum(rect.w, rect.h)); f32 halfW = width/2; mp_rect inner = {rect.x + halfW, rect.y + halfW, rect.w - width, rect.h - width}; mp_rect outer = {rect.x - halfW, rect.y - halfW, rect.w + width, rect.h + width}; mg_next_shape(canvas, attributes); mg_render_ellipse_fill_path(canvas, outer); mg_render_ellipse_fill_path(canvas, inner); } //------------------------------------------------------------------------------------------ //NOTE(martin): fonts //------------------------------------------------------------------------------------------ mg_font mg_font_create_from_memory(u32 size, byte* buffer, u32 rangeCount, unicode_range* ranges) { mg_font_data* fontData = malloc_type(mg_font_data); mg_font font = mg_font_alloc_handle(fontData); if(mg_font_is_nil(font)) { free(fontData); return(font); } stbtt_fontinfo stbttFontInfo; stbtt_InitFont(&stbttFontInfo, buffer, 0); //NOTE(martin): load font metrics data fontData->unitsPerEm = 1./stbtt_ScaleForMappingEmToPixels(&stbttFontInfo, 1); int ascent, descent, lineGap, x0, x1, y0, y1; stbtt_GetFontVMetrics(&stbttFontInfo, &ascent, &descent, &lineGap); stbtt_GetFontBoundingBox(&stbttFontInfo, &x0, &y0, &x1, &y1); fontData->extents.ascent = ascent; fontData->extents.descent = -descent; fontData->extents.leading = lineGap; fontData->extents.width = x1 - x0; stbtt_GetCodepointBox(&stbttFontInfo, 'x', &x0, &y0, &x1, &y1); fontData->extents.xHeight = y1 - y0; stbtt_GetCodepointBox(&stbttFontInfo, 'M', &x0, &y0, &x1, &y1); fontData->extents.capHeight = y1 - y0; //NOTE(martin): load codepoint ranges fontData->rangeCount = rangeCount; fontData->glyphMap = malloc_array(mg_glyph_map_entry, rangeCount); fontData->glyphCount = 0; for(int i=0; iglyphMap[i].range = ranges[i]; fontData->glyphMap[i].firstGlyphIndex = fontData->glyphCount + 1; fontData->glyphCount += ranges[i].count; } fontData->glyphs = malloc_array(mg_glyph_data, fontData->glyphCount); //NOTE(martin): first do a count of outlines int outlineCount = 0; for(int rangeIndex=0; rangeIndexglyphMap[rangeIndex].range.firstCodePoint; u32 firstGlyphIndex = fontData->glyphMap[rangeIndex].firstGlyphIndex; u32 endGlyphIndex = firstGlyphIndex + fontData->glyphMap[rangeIndex].range.count; for(int glyphIndex = firstGlyphIndex; glyphIndex < endGlyphIndex; glyphIndex++) { int stbttGlyphIndex = stbtt_FindGlyphIndex(&stbttFontInfo, codePoint); if(stbttGlyphIndex == 0) { //NOTE(martin): the codepoint is not found in the font codePoint++; continue; } //NOTE(martin): load glyph outlines stbtt_vertex* vertices = 0; outlineCount += stbtt_GetGlyphShape(&stbttFontInfo, stbttGlyphIndex, &vertices); stbtt_FreeShape(&stbttFontInfo, vertices); codePoint++; } } //NOTE(martin): allocate outlines fontData->outlines = malloc_array(mg_path_elt, outlineCount); fontData->outlineCount = 0; //NOTE(martin): load metrics and outlines for(int rangeIndex=0; rangeIndexglyphMap[rangeIndex].range.firstCodePoint; u32 firstGlyphIndex = fontData->glyphMap[rangeIndex].firstGlyphIndex; u32 endGlyphIndex = firstGlyphIndex + fontData->glyphMap[rangeIndex].range.count; for(int glyphIndex = firstGlyphIndex; glyphIndex < endGlyphIndex; glyphIndex++) { mg_glyph_data* glyph = &(fontData->glyphs[glyphIndex-1]); int stbttGlyphIndex = stbtt_FindGlyphIndex(&stbttFontInfo, codePoint); if(stbttGlyphIndex == 0) { //NOTE(martin): the codepoint is not found in the font, we zero the glyph info memset(glyph, 0, sizeof(*glyph)); codePoint++; continue; } glyph->exists = true; glyph->codePoint = codePoint; //NOTE(martin): load glyph metric int xAdvance, xBearing, x0, y0, x1, y1; stbtt_GetGlyphHMetrics(&stbttFontInfo, stbttGlyphIndex, &xAdvance, &xBearing); stbtt_GetGlyphBox(&stbttFontInfo, stbttGlyphIndex, &x0, &y0, &x1, &y1); glyph->extents.xAdvance = (f32)xAdvance; glyph->extents.yAdvance = 0; glyph->extents.xBearing = (f32)xBearing; glyph->extents.yBearing = y0; glyph->extents.width = x1 - x0; glyph->extents.height = y1 - y0; //NOTE(martin): load glyph outlines stbtt_vertex* vertices = 0; int vertexCount = stbtt_GetGlyphShape(&stbttFontInfo, stbttGlyphIndex, &vertices); glyph->pathDescriptor = (mg_path_descriptor){.startIndex = fontData->outlineCount, .count = vertexCount, .startPoint = {0, 0}}; mg_path_elt* elements = fontData->outlines + fontData->outlineCount; fontData->outlineCount += vertexCount; vec2 currentPos = {0, 0}; for(int vertIndex = 0; vertIndex < vertexCount; vertIndex++) { f32 x = vertices[vertIndex].x; f32 y = vertices[vertIndex].y; f32 cx = vertices[vertIndex].cx; f32 cy = vertices[vertIndex].cy; f32 cx1 = vertices[vertIndex].cx1; f32 cy1 = vertices[vertIndex].cy1; switch(vertices[vertIndex].type) { case STBTT_vmove: elements[vertIndex].type = MG_PATH_MOVE; elements[vertIndex].p[0] = (vec2){x, y}; break; case STBTT_vline: elements[vertIndex].type = MG_PATH_LINE; elements[vertIndex].p[0] = (vec2){x, y}; break; case STBTT_vcurve: { elements[vertIndex].type = MG_PATH_QUADRATIC; elements[vertIndex].p[0] = (vec2){cx, cy}; elements[vertIndex].p[1] = (vec2){x, y}; } break; case STBTT_vcubic: elements[vertIndex].type = MG_PATH_CUBIC; elements[vertIndex].p[0] = (vec2){cx, cy}; elements[vertIndex].p[1] = (vec2){cx1, cy1}; elements[vertIndex].p[2] = (vec2){x, y}; break; } currentPos = (vec2){x, y}; } stbtt_FreeShape(&stbttFontInfo, vertices); codePoint++; } } return(font); } void mg_font_destroy(mg_font fontHandle) { mg_font_data* fontData = mg_font_data_from_handle(fontHandle); if(!fontData) { return; } #ifdef DEBUG if(fontData->generation == UINT32_MAX) { LOG_ERROR("font info generation wrap around\n"); } #endif fontData->generation++; free(fontData->glyphMap); free(fontData->glyphs); free(fontData->outlines); free(fontData); mg_resource_handle_recycle(&__mgData.fonts, fontHandle.h); } str32 mg_font_get_glyph_indices_from_font_data(mg_font_data* fontData, str32 codePoints, str32 backing) { u64 count = minimum(codePoints.len, backing.len); for(int i = 0; irangeCount; rangeIndex++) { if(codePoints.ptr[i] >= fontData->glyphMap[rangeIndex].range.firstCodePoint && codePoints.ptr[i] < (fontData->glyphMap[rangeIndex].range.firstCodePoint + fontData->glyphMap[rangeIndex].range.count)) { u32 rangeOffset = codePoints.ptr[i] - fontData->glyphMap[rangeIndex].range.firstCodePoint; glyphIndex = fontData->glyphMap[rangeIndex].firstGlyphIndex + rangeOffset; break; } } if(glyphIndex && !fontData->glyphs[glyphIndex].exists) { backing.ptr[i] = 0; } backing.ptr[i] = glyphIndex; } str32 res = {.len = count, .ptr = backing.ptr}; return(res); } u32 mg_font_get_glyph_index_from_font_data(mg_font_data* fontData, utf32 codePoint) { u32 glyphIndex = 0; str32 codePoints = {1, &codePoint}; str32 backing = {1, &glyphIndex}; mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing); return(glyphIndex); } str32 mg_font_get_glyph_indices(mg_font font, str32 codePoints, str32 backing) { mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return((str32){0}); } return(mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing)); } str32 mg_font_push_glyph_indices(mg_font font, mem_arena* arena, str32 codePoints) { u32* buffer = mem_arena_alloc_array(arena, u32, codePoints.len); str32 backing = {codePoints.len, buffer}; return(mg_font_get_glyph_indices(font, codePoints, backing)); } u32 mg_font_get_glyph_index(mg_font font, utf32 codePoint) { u32 glyphIndex = 0; str32 codePoints = {1, &codePoint}; str32 backing = {1, &glyphIndex}; mg_font_get_glyph_indices(font, codePoints, backing); return(glyphIndex); } mg_glyph_data* mg_font_get_glyph_data(mg_font_data* fontData, u32 glyphIndex) { DEBUG_ASSERT(glyphIndex); DEBUG_ASSERT(glyphIndex < fontData->glyphCount); return(&(fontData->glyphs[glyphIndex-1])); } mg_font_extents mg_font_get_extents(mg_font font) { mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return((mg_font_extents){0}); } return(fontData->extents); } mg_font_extents mg_font_get_scaled_extents(mg_font font, f32 emSize) { mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return((mg_font_extents){0}); } f32 scale = emSize/fontData->unitsPerEm; mg_font_extents extents = fontData->extents; extents.ascent *= scale; extents.descent *= scale; extents.leading *= scale; extents.xHeight *= scale; extents.capHeight *= scale; extents.width *= scale; return(extents); } f32 mg_font_get_scale_for_em_pixels(mg_font font, f32 emSize) { mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return(0); } return(emSize/fontData->unitsPerEm); } void mg_font_get_glyph_extents_from_font_data(mg_font_data* fontData, str32 glyphIndices, mg_text_extents* outExtents) { for(int i=0; i= fontData->glyphCount) { continue; } mg_glyph_data* glyph = mg_font_get_glyph_data(fontData, glyphIndices.ptr[i]); outExtents[i] = glyph->extents; } } int mg_font_get_glyph_extents(mg_font font, str32 glyphIndices, mg_text_extents* outExtents) { mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return(-1); } mg_font_get_glyph_extents_from_font_data(fontData, glyphIndices, outExtents); return(0); } int mg_font_get_codepoint_extents(mg_font font, utf32 codePoint, mg_text_extents* outExtents) { mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return(-1); } u32 glyphIndex = 0; str32 codePoints = {1, &codePoint}; str32 backing = {1, &glyphIndex}; str32 glyphs = mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing); mg_font_get_glyph_extents_from_font_data(fontData, glyphs, outExtents); return(0); } mp_rect mg_text_bounding_box_utf32(mg_font font, f32 fontSize, str32 codePoints) { if(!codePoints.len || !codePoints.ptr) { return((mp_rect){0}); } mg_font_data* fontData = mg_font_data_from_handle(font); if(!fontData) { return((mp_rect){0}); } mem_arena* scratch = mem_scratch(); str32 glyphIndices = mg_font_push_glyph_indices(font, scratch, codePoints); //NOTE(martin): find width of missing character //TODO(martin): should cache that at font creation... mg_text_extents missingGlyphExtents; u32 missingGlyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 0xfffd); if(missingGlyphIndex) { mg_font_get_glyph_extents_from_font_data(fontData, (str32){1, &missingGlyphIndex}, &missingGlyphExtents); } else { //NOTE(martin): could not find replacement glyph, try to get an 'x' to get a somewhat correct width // to render an empty rectangle. Otherwise just render with the max font width f32 boxWidth = fontData->extents.width * 0.8; f32 xBearing = fontData->extents.width * 0.1; f32 xAdvance = fontData->extents.width; missingGlyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 'x'); if(missingGlyphIndex) { mg_font_get_glyph_extents_from_font_data(fontData, (str32){1, &missingGlyphIndex}, &missingGlyphExtents); } else { missingGlyphExtents.xBearing = fontData->extents.width * 0.1; missingGlyphExtents.yBearing = 0; missingGlyphExtents.width = fontData->extents.width * 0.8; missingGlyphExtents.xAdvance = fontData->extents.width; missingGlyphExtents.yAdvance = 0; } } //NOTE(martin): accumulate text extents f32 width = 0; f32 x = 0; f32 y = 0; f32 lineHeight = fontData->extents.descent + fontData->extents.ascent; for(int i=0; i= fontData->glyphCount) { extents = missingGlyphExtents; } else { glyph = mg_font_get_glyph_data(fontData, glyphIndices.ptr[i]); extents = glyph->extents; } x += extents.xAdvance; y += extents.yAdvance; if(glyph && glyph->codePoint == '\n') { width = maximum(width, x); x = 0; y += lineHeight + fontData->extents.leading; } } width = maximum(width, x); f32 fontScale = mg_font_get_scale_for_em_pixels(font, fontSize); mp_rect rect = {0, -fontData->extents.ascent * fontScale, width * fontScale, (y + lineHeight) * fontScale }; return(rect); } mp_rect mg_text_bounding_box(mg_font font, f32 fontSize, str8 text) { if(!text.len || !text.ptr) { return((mp_rect){0}); } mem_arena* scratch = mem_scratch(); str32 codePoints = utf8_push_to_codepoints(scratch, text); return(mg_text_bounding_box_utf32(font, fontSize, codePoints)); } //------------------------------------------------------------------------------------------ //NOTE(martin): graphics canvas API //------------------------------------------------------------------------------------------ #if MG_COMPILE_BACKEND_METAL mg_canvas_backend* mg_mtl_canvas_create(mg_surface surface); #endif #if MG_COMPILE_BACKEND_GL mg_canvas_backend* mg_gl_canvas_create(mg_surface surface); #endif mg_canvas mg_canvas_create(mg_surface surface) { mg_canvas canvas = mg_canvas_nil(); mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); if(surfaceData) { mg_canvas_backend* backend = 0; switch(surfaceData->backend) { #if MG_COMPILE_BACKEND_METAL case MG_BACKEND_METAL: backend = mg_mtl_canvas_create(surface); break; #endif #if MG_COMPILE_BACKEND_GL case MG_BACKEND_GL: backend = mg_gl_canvas_create(surface); break; #endif default: break; } if(backend) { mg_canvas_data* canvasData = malloc_type(mg_canvas_data); memset(canvasData, 0, sizeof(mg_canvas_data)); canvasData->backend = backend; canvasData->primitiveCount = 0; canvasData->path.startIndex = 0; canvasData->path.count = 0; canvasData->nextShapeIndex = 0; canvasData->attributes.color = (mg_color){0, 0, 0, 1}; canvasData->attributes.tolerance = 1; canvasData->attributes.width = 10; canvasData->attributes.clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; canvasData->transform = (mg_mat2x3){{1, 0, 0, 0, 1, 0}}; //TODO: review this //////////////////////////////////////////////////////////////////////// canvas = mg_canvas_alloc_handle(canvasData); mg_canvas_set_current(canvas); //NOTE: create a blank image //WARN: this requires setting the current context before u8 bytes[4] = {255, 255, 255, 255}; canvasData->blankImage = mg_image_create_from_rgba8(1, 1, bytes); //////////////////////////////////////////////////////////////////////////////////////////////////// } } return(canvas); } void mg_canvas_destroy(mg_canvas handle) { mg_canvas_data* canvas = mg_canvas_data_from_handle(handle); if(canvas) { if(__mgCurrentCanvas == canvas) { __mgCurrentCanvas = 0; __mgCurrentCanvasHandle = mg_canvas_nil(); } if(canvas->backend && canvas->backend->destroy) { canvas->backend->destroy(canvas->backend); } free(canvas); mg_resource_handle_recycle(&__mgData.canvases, handle.h); } } mg_canvas mg_canvas_set_current(mg_canvas canvas) { mg_canvas old = __mgCurrentCanvasHandle; __mgCurrentCanvasHandle = canvas; __mgCurrentCanvas = mg_canvas_data_from_handle(canvas); return(old); } //////////////////////////////////////////////////////////// mp_rect mg_clip_stack_top(mg_canvas_data* canvas) { if(canvas->clipStackSize == 0) { return((mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}); } else { return(canvas->clipStack[canvas->clipStackSize-1]); } } void mg_clip_stack_push(mg_canvas_data* canvas, mp_rect clip) { if(canvas->clipStackSize >= MG_CLIP_STACK_MAX_DEPTH) { LOG_ERROR("clip stack overflow\n"); } else { canvas->clipStack[canvas->clipStackSize] = clip; canvas->clipStackSize++; canvas->clip = clip; } } void mg_clip_stack_pop(mg_canvas_data* canvas) { if(canvas->clipStackSize == 0) { LOG_ERROR("clip stack underflow\n"); } else { canvas->clipStackSize--; canvas->clip = mg_clip_stack_top(canvas); } } void mg_do_clip_push(mg_canvas_data* canvas, mp_rect clip) { //NOTE(martin): transform clip vec2 p0 = mg_mat2x3_mul(canvas->transform, (vec2){clip.x, clip.y}); vec2 p1 = mg_mat2x3_mul(canvas->transform, (vec2){clip.x + clip.w, clip.y}); vec2 p2 = mg_mat2x3_mul(canvas->transform, (vec2){clip.x + clip.w, clip.y + clip.h}); vec2 p3 = mg_mat2x3_mul(canvas->transform, (vec2){clip.x, clip.y + clip.h}); f32 x0 = minimum(p0.x, minimum(p1.x, minimum(p2.x, p3.x))); f32 y0 = minimum(p0.y, minimum(p1.y, minimum(p2.y, p3.y))); f32 x1 = maximum(p0.x, maximum(p1.x, maximum(p2.x, p3.x))); f32 y1 = maximum(p0.y, maximum(p1.y, maximum(p2.y, p3.y))); mp_rect current = mg_clip_stack_top(canvas); //NOTE(martin): intersect with current clip x0 = maximum(current.x, x0); y0 = maximum(current.y, y0); x1 = minimum(current.x + current.w, x1); y1 = minimum(current.y + current.h, y1); mp_rect r = {x0, y0, maximum(0, x1-x0), maximum(0, y1-y0)}; mg_clip_stack_push(canvas, r); } void mg_flush_batch(mg_canvas_data* canvas, mg_image_data* image) { mg_finalize_shape(canvas); if(canvas->backend && canvas->backend->drawBatch && canvas->indexCount) { canvas->backend->drawBatch(canvas->backend, image, canvas->nextShapeIndex, canvas->vertexCount, canvas->indexCount); } mg_reset_shape_index(canvas); canvas->vertexCount = 0; canvas->indexCount = 0; canvas->shapeFirstVertexIndex = 0; } void mg_flush_commands(int primitiveCount, mg_primitive* primitives, mg_path_elt* pathElements) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_color clearColor = {0, 0, 0, 1}; u32 nextIndex = 0; mg_reset_shape_index(canvas); canvas->clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; canvas->currentImage = mg_image_nil(); canvas->backend->begin(canvas->backend); for(int i=0; i= primitiveCount) { LOG_ERROR("invalid location '%i' in graphics command buffer would cause an overrun\n", nextIndex); break; } mg_primitive* primitive = &(primitives[nextIndex]); nextIndex++; canvas->transform = primitive->transform; if(i && primitive->attributes.image.h != canvas->currentImage.h) { mg_image_data* imageData = mg_image_data_from_handle(canvas, canvas->currentImage); mg_flush_batch(canvas, imageData); canvas->currentImage = primitive->attributes.image; } switch(primitive->cmd) { case MG_CMD_CLEAR: { //NOTE(martin): clear buffers canvas->vertexCount = 0; canvas->indexCount = 0; canvas->backend->clear(canvas->backend, primitive->attributes.color); } break; case MG_CMD_FILL: { mg_next_shape(canvas, &primitive->attributes); mg_render_fill(canvas, pathElements + primitive->path.startIndex, &primitive->path); } break; case MG_CMD_STROKE: { mg_render_stroke(canvas, pathElements + primitive->path.startIndex, &primitive->path, &primitive->attributes); } break; case MG_CMD_RECT_FILL: mg_render_rectangle_fill(canvas, primitive->rect, &primitive->attributes); break; case MG_CMD_RECT_STROKE: mg_render_rectangle_stroke(canvas, primitive->rect, &primitive->attributes); break; case MG_CMD_ROUND_RECT_FILL: mg_render_rounded_rectangle_fill(canvas, primitive->roundedRect, &primitive->attributes); break; case MG_CMD_ROUND_RECT_STROKE: mg_render_rounded_rectangle_stroke(canvas, primitive->roundedRect, &primitive->attributes); break; case MG_CMD_ELLIPSE_FILL: mg_render_ellipse_fill(canvas, primitive->rect, &primitive->attributes); break; case MG_CMD_ELLIPSE_STROKE: mg_render_ellipse_stroke(canvas, primitive->rect, &primitive->attributes); break; case MG_CMD_JUMP: { if(primitive->jump == ~0) { //NOTE(martin): normal end of stream marker goto exit_command_loop; } else if(primitive->jump >= primitiveCount) { LOG_ERROR("invalid jump location '%i' in graphics command buffer\n", primitive->jump); goto exit_command_loop; } else { nextIndex = primitive->jump; } } break; case MG_CMD_CLIP_PUSH: { //TODO(martin): use only aligned rect and avoid this mp_rect r = {primitive->rect.x, primitive->rect.y, primitive->rect.w, primitive->rect.h}; mg_do_clip_push(canvas, r); } break; case MG_CMD_CLIP_POP: { mg_clip_stack_pop(canvas); } break; } } exit_command_loop: ; mg_image_data* imageData = mg_image_data_from_handle(canvas, canvas->currentImage); mg_flush_batch(canvas, imageData); canvas->backend->end(canvas->backend); //NOTE(martin): clear buffers canvas->vertexCount = 0; canvas->indexCount = 0; } void mg_flush() { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_flush_commands(canvas->primitiveCount, canvas->primitives, canvas->pathElements); canvas->primitiveCount = 0; canvas->path.startIndex = 0; canvas->path.count = 0; canvas->frameCounter++; } //------------------------------------------------------------------------------------------ //NOTE(martin): transform, viewport and clipping //------------------------------------------------------------------------------------------ void mg_matrix_push(mg_mat2x3 matrix) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_mat2x3 transform = mg_matrix_stack_top(canvas); mg_matrix_stack_push(canvas, mg_mat2x3_mul_m(transform, matrix)); } } void mg_matrix_pop() { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_matrix_stack_pop(canvas); } } void mg_clip_push(f32 x, f32 y, f32 w, f32 h) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_CLIP_PUSH, .rect = (mp_rect){x, y, w, h}}); } } void mg_clip_pop() { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_CLIP_POP}); } } //------------------------------------------------------------------------------------------ //NOTE(martin): graphics attributes setting/getting //------------------------------------------------------------------------------------------ void mg_set_color(mg_color color) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.color = color; } } void mg_set_color_rgba(f32 r, f32 g, f32 b, f32 a) { mg_set_color((mg_color){r, g, b, a}); } void mg_set_width(f32 width) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.width = width; } } void mg_set_tolerance(f32 tolerance) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.tolerance = tolerance; } } void mg_set_joint(mg_joint_type joint) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.joint = joint; } } void mg_set_max_joint_excursion(f32 maxJointExcursion) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.maxJointExcursion = maxJointExcursion; } } void mg_set_cap(mg_cap_type cap) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.cap = cap; } } void mg_set_font(mg_font font) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.font = font; } } void mg_set_font_size(f32 fontSize) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.fontSize = fontSize; } } void mg_set_text_flip(bool flip) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->textFlip = flip; } } void mg_set_image(mg_image image) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.image = image; vec2 size = mg_image_size(image); canvas->attributes.srcRegion = (mp_rect){0, 0, size.x, size.y}; } } void mg_set_image_source_region(mp_rect region) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { canvas->attributes.srcRegion = region; } } mg_color mg_get_color() { mg_color color = {0}; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { color = canvas->attributes.color; } return(color); } f32 mg_get_width() { f32 width = 0; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { width = canvas->attributes.width; } return(width); } f32 mg_get_tolerance() { f32 tolerance = 0; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { tolerance = canvas->attributes.tolerance; } return(tolerance); } mg_joint_type mg_get_joint() { mg_joint_type joint = 0; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { joint = canvas->attributes.joint; } return(joint); } f32 mg_get_max_joint_excursion() { f32 maxJointExcursion = 0; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { maxJointExcursion = canvas->attributes.maxJointExcursion; } return(maxJointExcursion); } mg_cap_type mg_get_cap() { mg_cap_type cap = 0; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { cap = canvas->attributes.cap; } return(cap); } mg_font mg_get_font() { mg_font font = mg_font_nil(); mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { font = canvas->attributes.font; } return(font); } f32 mg_get_font_size() { f32 fontSize = 0; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { fontSize = canvas->attributes.fontSize; } return(fontSize); } bool mg_get_text_flip() { bool flip = false; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { flip = canvas->textFlip; } return(flip); } //------------------------------------------------------------------------------------------ //NOTE(martin): path construction //------------------------------------------------------------------------------------------ vec2 mg_get_position() { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return((vec2){0, 0}); } return(canvas->subPathLastPoint); } void mg_move_to(f32 x, f32 y) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_MOVE, .p[0] = {x, y}})); canvas->subPathStartPoint = (vec2){x, y}; canvas->subPathLastPoint = (vec2){x, y}; } void mg_line_to(f32 x, f32 y) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_LINE, .p[0] = {x, y}})); canvas->subPathLastPoint = (vec2){x, y}; } void mg_quadratic_to(f32 x1, f32 y1, f32 x2, f32 y2) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_QUADRATIC, .p = {{x1, y1}, {x2, y2}}})); canvas->subPathLastPoint = (vec2){x2, y2}; } void mg_cubic_to(f32 x1, f32 y1, f32 x2, f32 y2, f32 x3, f32 y3) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_CUBIC, .p = {{x1, y1}, {x2, y2}, {x3, y3}}})); canvas->subPathLastPoint = (vec2){x3, y3}; } void mg_close_path() { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } if( canvas->subPathStartPoint.x != canvas->subPathLastPoint.x || canvas->subPathStartPoint.y != canvas->subPathLastPoint.y) { mg_line_to(canvas->subPathStartPoint.x, canvas->subPathStartPoint.y); } canvas->subPathStartPoint = canvas->subPathLastPoint; } mp_rect mg_glyph_outlines_from_font_data(mg_font_data* fontData, str32 glyphIndices) { mg_canvas_data* canvas = __mgCurrentCanvas; f32 startX = canvas->subPathLastPoint.x; f32 startY = canvas->subPathLastPoint.y; f32 maxWidth = 0; f32 scale = canvas->attributes.fontSize/fontData->unitsPerEm; for(int i=0; isubPathLastPoint.x; f32 yOffset = canvas->subPathLastPoint.y; f32 flip = canvas->textFlip ? -1 : 1; if(!glyphIndex || glyphIndex >= fontData->glyphCount) { LOG_WARNING("code point is not present in font ranges\n"); //NOTE(martin): try to find the replacement character glyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 0xfffd); if(!glyphIndex) { //NOTE(martin): could not find replacement glyph, try to get an 'x' to get a somewhat correct width // to render an empty rectangle. Otherwise just render with the max font width f32 boxWidth = fontData->extents.width * 0.8; f32 xBearing = fontData->extents.width * 0.1; f32 xAdvance = fontData->extents.width; glyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 'x'); if(glyphIndex) { mg_glyph_data* glyph = &(fontData->glyphs[glyphIndex]); boxWidth = glyph->extents.width; xBearing = glyph->extents.xBearing; xAdvance = glyph->extents.xAdvance; } f32 oldStrokeWidth = canvas->attributes.width; mg_set_width(boxWidth*0.005); mg_rectangle_stroke(xOffset + xBearing * scale, yOffset, boxWidth * scale * flip, fontData->extents.capHeight*scale); mg_set_width(oldStrokeWidth); mg_move_to(xOffset + xAdvance * scale, yOffset); maxWidth = maximum(maxWidth, xOffset + xAdvance*scale - startX); continue; } } mg_glyph_data* glyph = mg_font_get_glyph_data(fontData, glyphIndex); mg_path_push_elements(canvas, glyph->pathDescriptor.count, fontData->outlines + glyph->pathDescriptor.startIndex); mg_path_elt* elements = canvas->pathElements + canvas->path.count + canvas->path.startIndex - glyph->pathDescriptor.count; for(int eltIndex=0; eltIndexpathDescriptor.count; eltIndex++) { for(int pIndex = 0; pIndex < 3; pIndex++) { elements[eltIndex].p[pIndex].x = elements[eltIndex].p[pIndex].x * scale + xOffset; elements[eltIndex].p[pIndex].y = elements[eltIndex].p[pIndex].y * scale * flip + yOffset; } } mg_move_to(xOffset + scale*glyph->extents.xAdvance, yOffset); maxWidth = maximum(maxWidth, xOffset + scale*glyph->extents.xAdvance - startX); } f32 lineHeight = (fontData->extents.ascent + fontData->extents.descent)*scale; mp_rect box = {startX, startY, maxWidth, canvas->subPathLastPoint.y - startY + lineHeight }; return(box); } mp_rect mg_glyph_outlines(str32 glyphIndices) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return((mp_rect){0}); } mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); if(!fontData) { return((mp_rect){0}); } return(mg_glyph_outlines_from_font_data(fontData, glyphIndices)); } void mg_codepoints_outlines(str32 codePoints) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); if(!fontData) { return; } str32 glyphIndices = mg_font_push_glyph_indices(canvas->attributes.font, mem_scratch(), codePoints); mg_glyph_outlines_from_font_data(fontData, glyphIndices); } void mg_text_outlines(str8 text) { mg_canvas_data* canvas = __mgCurrentCanvas; if(!canvas) { return; } mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); if(!fontData) { return; } mem_arena* scratch = mem_scratch(); str32 codePoints = utf8_push_to_codepoints(scratch, text); str32 glyphIndices = mg_font_push_glyph_indices(canvas->attributes.font, scratch, codePoints); mg_glyph_outlines_from_font_data(fontData, glyphIndices); } //------------------------------------------------------------------------------------------ //NOTE(martin): clear/fill/stroke //------------------------------------------------------------------------------------------ void mg_clear() { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_CLEAR, .attributes = canvas->attributes}); } } void mg_fill() { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas && canvas->path.count) { mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_FILL, .path = canvas->path, .attributes = canvas->attributes}); mg_new_path(canvas); } } void mg_stroke() { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas && canvas->path.count) { mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_STROKE, .path = canvas->path, .attributes = canvas->attributes}); mg_new_path(canvas); } } //------------------------------------------------------------------------------------------ //NOTE(martin): 'fast' shapes primitives //------------------------------------------------------------------------------------------ void mg_rectangle_fill(f32 x, f32 y, f32 w, f32 h) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_RECT_FILL, .rect = (mp_rect){x, y, w, h}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_rectangle_stroke(f32 x, f32 y, f32 w, f32 h) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_RECT_STROKE, .rect = (mp_rect){x, y, w, h}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_rounded_rectangle_fill(f32 x, f32 y, f32 w, f32 h, f32 r) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ROUND_RECT_FILL, .roundedRect = (mg_rounded_rect){x, y, w, h, r}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_rounded_rectangle_stroke(f32 x, f32 y, f32 w, f32 h, f32 r) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ROUND_RECT_STROKE, .roundedRect = (mg_rounded_rect){x, y, w, h, r}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_circle_fill(f32 x, f32 y, f32 r) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ELLIPSE_FILL, .rect = (mp_rect){x-r, y-r, 2*r, 2*r}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_circle_stroke(f32 x, f32 y, f32 r) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ELLIPSE_STROKE, .rect = (mp_rect){x-r, y-r, 2*r, 2*r}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_ellipse_fill(f32 x, f32 y, f32 rx, f32 ry) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ELLIPSE_FILL, .rect = (mp_rect){x-rx, y-ry, 2*rx, 2*ry}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } void mg_ellipse_stroke(f32 x, f32 y, f32 rx, f32 ry) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ELLIPSE_STROKE, .rect = (mp_rect){x-rx, y-ry, 2*rx, 2*ry}, .attributes = canvas->attributes}; mg_push_command(canvas, primitive); } } //TODO: change to arc_to? void mg_arc(f32 x, f32 y, f32 r, f32 arcAngle, f32 startAngle) { f32 endAngle = startAngle + arcAngle; while(startAngle < endAngle) { f32 smallAngle = minimum(endAngle - startAngle, M_PI/4.); if(smallAngle < 0.001) { break; } vec2 v0 = {cos(smallAngle/2), sin(smallAngle/2)}; vec2 v1 = {(4-v0.x)/3, (1-v0.x)*(3-v0.x)/(3*v0.y)}; vec2 v2 = {v1.x, -v1.y}; vec2 v3 = {v0.x, -v0.y}; f32 rotAngle = smallAngle/2 + startAngle; f32 rotCos = cos(rotAngle); f32 rotSin = sin(rotAngle); mg_mat2x3 t = {r*rotCos, -r*rotSin, x, r*rotSin, r*rotCos, y}; v0 = mg_mat2x3_mul(t, v0); v1 = mg_mat2x3_mul(t, v1); v2 = mg_mat2x3_mul(t, v2); v3 = mg_mat2x3_mul(t, v3); mg_move_to(v0.x, v0.y); mg_cubic_to(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y); startAngle += smallAngle; } } //------------------------------------------------------------------------------------------ //NOTE(martin): images //------------------------------------------------------------------------------------------ MP_API mg_image mg_image_create(u32 width, u32 height) { mg_image image = mg_image_nil(); mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_image_data* imageData = canvas->backend->imageCreate(canvas->backend, (vec2){width, height}); if(imageData) { image = mg_image_alloc_handle(canvas, imageData); } } return(image); } MP_API mg_image mg_image_create_from_rgba8(u32 width, u32 height, u8* pixels) { mg_image image = mg_image_create(width, height); if(!mg_image_is_nil(image)) { mg_image_upload_region_rgba8(image, (mp_rect){0, 0, width, height}, pixels); } return(image); } MP_API mg_image mg_image_create_from_data(str8 data, bool flip) { mg_image image = mg_image_nil(); int width, height, channels; stbi_set_flip_vertically_on_load(flip ? 1 : 0); u8* pixels = stbi_load_from_memory((u8*)data.ptr, data.len, &width, &height, &channels, 4); if(pixels) { image = mg_image_create_from_rgba8(width, height, pixels); free(pixels); } return(image); } MP_API mg_image mg_image_create_from_file(str8 path, bool flip) { mg_image image = mg_image_nil(); int width, height, channels; const char* cpath = str8_to_cstring(mem_scratch(), path); stbi_set_flip_vertically_on_load(flip ? 1 : 0); u8* pixels = stbi_load(cpath, &width, &height, &channels, 4); if(pixels) { image = mg_image_create_from_rgba8(width, height, pixels); free(pixels); } return(image); } MP_API void mg_image_destroy(mg_image image) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_image_data* imageData = mg_image_data_from_handle(canvas, image); if(imageData) { canvas->backend->imageDestroy(canvas->backend, imageData); mg_resource_handle_recycle(&canvas->imagePool, image.h); } } } MP_API void mg_image_upload_region_rgba8(mg_image image, mp_rect region, u8* pixels) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_image_data* imageData = mg_image_data_from_handle(canvas, image); if(imageData) { canvas->backend->imageUploadRegion(canvas->backend, imageData, region, pixels); } } } MP_API vec2 mg_image_size(mg_image image) { vec2 res = {0}; mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_image_data* imageData = mg_image_data_from_handle(canvas, image); if(imageData) { res = imageData->size; } } return(res); } MP_API void mg_image_draw_region(mg_image image, mp_rect srcRegion, mp_rect dstRegion) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_RECT_FILL, .rect = dstRegion, .attributes = canvas->attributes}; primitive.attributes.image = image; primitive.attributes.color = (mg_color){1, 1, 1, 1}; primitive.attributes.srcRegion = srcRegion; mg_push_command(canvas, primitive); } } MP_API void mg_image_draw_region_rounded(mg_image image, mp_rect srcRegion, mp_rect dstRegion, f32 roundness) { mg_canvas_data* canvas = __mgCurrentCanvas; if(canvas) { mg_primitive primitive = {.cmd = MG_CMD_ROUND_RECT_FILL, .roundedRect = {dstRegion.x, dstRegion.y, dstRegion.w, dstRegion.h, roundness}, .attributes = canvas->attributes}; primitive.attributes.image = image; primitive.attributes.color = (mg_color){1, 1, 1, 1}; primitive.attributes.srcRegion = srcRegion; mg_push_command(canvas, primitive); } } MP_API void mg_image_draw(mg_image image, mp_rect rect) { vec2 size = mg_image_size(image); mg_image_draw_region(image, (mp_rect){0, 0, size.x, size.y}, rect); } MP_API void mg_image_draw_rounded(mg_image image, mp_rect rect, f32 roundness) { vec2 size = mg_image_size(image); mg_image_draw_region_rounded(image, (mp_rect){0, 0, size.x, size.y}, rect, roundness); } #undef LOG_SUBSYSTEM