pebble/src/fw/applib/graphics/graphics_line.c
Josh Soref a169a706b7 spelling: supported
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-29 00:03:28 -05:00

830 lines
31 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "graphics_line.h"
#include "graphics_private.h"
#include "graphics.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/swap.h"
#define MINIMUM_PRECISE_STROKE_WIDTH 2
// Precomputed lookup table with quadrant of the circle for the caps on stroked lines
// table of y-coordinates expressed as Fixed_S16_3.raw_value
// for each x-coordinate (array index) of first quadrant of unit circle
// see prv_calc_quadrant_lookup()
static const uint16_t s_circle_table[] = {
8,
16, 3,
24, 7, 2,
32, 11, 5, 2,
40, 16, 8, 4, 1,
48, 22, 13, 7, 3, 1,
56, 28, 17, 11, 6, 3, 1,
64, 34, 22, 15, 9, 5, 3, 1,
72, 40, 27, 19, 13, 8, 5, 2, 1,
80, 46, 32, 23, 16, 11, 7, 4, 2, 1,
88, 52, 38, 28, 21, 15, 10, 7, 4, 2, 1,
96, 58, 43, 33, 25, 19, 13, 9, 6, 4, 2, 1,
104, 64, 49, 38, 29, 23, 17, 12, 8, 6, 3, 2, 1
};
MOCKABLE void graphics_line_draw_1px_non_aa(GContext* ctx, GPoint p0, GPoint p1) {
p0.x += ctx->draw_state.drawing_box.origin.x;
p1.x += ctx->draw_state.drawing_box.origin.x;
p0.y += ctx->draw_state.drawing_box.origin.y;
p1.y += ctx->draw_state.drawing_box.origin.y;
int steep = abs(p1.y - p0.y) > abs(p1.x - p0.x);
if (steep) {
swap16(&p0.x, &p0.y);
swap16(&p1.x, &p1.y);
}
if (p0.x > p1.x) {
swap16(&p0.x, &p1.x);
swap16(&p0.y, &p1.y);
}
int dx = p1.x - p0.x;
int dy = abs(p1.y - p0.y);
int16_t err = dx / 2;
int16_t ystep;
if (p0.y < p1.y) {
ystep = 1;
} else {
ystep = -1;
}
for (; p0.x <= p1.x; p0.x++) {
if (steep) {
graphics_private_set_pixel(ctx, GPoint(p0.y, p0.x));
} else {
graphics_private_set_pixel(ctx, GPoint(p0.x, p0.y));
}
err -= dy;
if (err < 0) {
p0.y += ystep;
err += dx;
}
}
}
#if PBL_COLOR
MOCKABLE void graphics_line_draw_1px_aa(GContext* ctx, GPoint p0, GPoint p1) {
// Implementation of Wu-Xiang fast anti-aliased line drawing algorithm
// Points over which we're going to iterate adjusted to drawing_box
int16_t x1 = p0.x + ctx->draw_state.drawing_box.origin.x;
int16_t y1 = p0.y + ctx->draw_state.drawing_box.origin.y;
int16_t x2 = p1.x + ctx->draw_state.drawing_box.origin.x;
int16_t y2 = p1.y + ctx->draw_state.drawing_box.origin.y;
// Main loop helpers
uint16_t intensity_shift, error_adj, error_acc;
uint16_t error_acc_temp, weighting, weighting_complement_mask;
int16_t dx, dy, tmp, xi;
// Grabbing framebuffer for drawing and stroke color to blend
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
GColor stroke_color = ctx->draw_state.stroke_color;
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
// Make sure the line runs top to bottom
if (y1 > y2) {
tmp = y1; y1 = y2; y2 = tmp;
tmp = x1; x1 = x2; x2 = tmp;
}
// Draw the initial pixel
// TODO: PBL-14743: Make a unit test that will test case of .frame.origin != {0,0}
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x1, y1, MAX_PLOT_OPACITY,
stroke_color);
if ((dx = x2 - x1) >= 0) {
xi = 1;
} else {
xi = -1;
dx = -dx;
}
// If line is vertical, horizontal or diagonal we dont need to anti-alias it
if ((dy = y2 - y1) == 0) {
// Horizontal line
int16_t start = x1;
int16_t end = x1 + (dx * xi);
if (end < start) {
swap16(&start, &end);
}
graphics_private_draw_horizontal_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, y1,
(Fixed_S16_3) {.integer = start},
(Fixed_S16_3) {.integer = end}, stroke_color);
} else if (dx == 0) {
// Vertical line
graphics_private_draw_vertical_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, x1,
(Fixed_S16_3){.integer = y1},
(Fixed_S16_3){.integer = y1 + dy}, stroke_color);
} else if (dx == dy) {
// Diagonal line
while (dy-- != 0) {
x1 += xi;
y1++;
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x1, y1, MAX_PLOT_OPACITY,
stroke_color);
}
} else {
// Line is not horizontal, diagonal, or vertical
// Error accumulator
error_acc = 0;
// # of bits by which to shift error_acc to get intensity level
intensity_shift = 14;
// Mask used to flip all bits in an intensity weighting
// producing the result (1 - intensity weighting)
weighting_complement_mask = MAX_PLOT_BRIGHTNESS;
// Is this an X-major or Y-major line?
if (dy > dx) {
// Y-major line; calculate 16-bit fixed-point fractional part of a
// pixel that X advances each time Y advances 1 pixel, truncating the
// result so that we won't overrun the endpoint along the X axis
error_adj = ((uint32_t)(dx) << 16) / (uint32_t) dy;
// Draw all pixels other than the first and last
while (--dy) {
error_acc_temp = error_acc;
error_acc += error_adj;
if (error_acc <= error_acc_temp) {
// The error accumulator turned over, so advance the X coord
x1 += xi;
}
y1++;
// The IntensityBits most significant bits of error_acc give us the
// intensity weighting for this pixel, and the complement of the
// weighting for the paired pixel
weighting = error_acc >> intensity_shift;
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x1, y1, weighting,
stroke_color);
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x1 + xi, y1,
(weighting ^ weighting_complement_mask), stroke_color);
}
// Draw final pixel
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x2, y2, MAX_PLOT_OPACITY,
stroke_color);
} else {
// It's an X-major line
error_adj = ((uint32_t) dy << 16) / (uint32_t) dx;
// Draw all pixels other than the first and last
while (--dx) {
error_acc_temp = error_acc;
error_acc += error_adj;
if (error_acc <= error_acc_temp) {
// The error accumulator turned over, so advance the Y coord
y1++;
}
x1 += xi;
weighting = error_acc >> intensity_shift;
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x1, y1, weighting,
stroke_color);
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x1, y1 + 1,
(weighting ^ weighting_complement_mask), stroke_color);
}
// Draw the final pixel
graphics_private_plot_pixel(framebuffer, &ctx->draw_state.clip_box, x2, y2, MAX_PLOT_OPACITY,
stroke_color);
}
}
// Release the framebuffer after we're done
graphics_release_frame_buffer(ctx, framebuffer);
}
#endif // PBL_COLOR
static Fixed_S16_3 prv_get_circle_border_precise(int16_t y, uint16_t radius) {
// This is so we operate in middle of the pixel, not on the edge
y += FIXED_S16_3_ONE.raw_value / 2;
return (Fixed_S16_3){.raw_value = radius - integer_sqrt(radius * radius - y * y)};
}
static void prv_calc_cap_prepared(Fixed_S16_3 cap_center, Fixed_S16_3 cap_center_offset,
Fixed_S16_3 cap_radius, Fixed_S16_3 progress, Fixed_S16_3 *min, Fixed_S16_3 *max) {
if (progress.raw_value >= cap_center.raw_value - cap_radius.raw_value &&
progress.raw_value <= cap_center.raw_value + cap_radius.raw_value) {
int16_t circle_min;
int16_t circle_max;
const int16_t p_offset = cap_center_offset.raw_value;
const int16_t r8 = cap_radius.raw_value;
if (progress.raw_value <= cap_center.raw_value) {
// Top part of the circle
Fixed_S16_3 lookup_val = prv_get_circle_border_precise(cap_center.raw_value -
progress.raw_value, cap_radius.raw_value + FIXED_S16_3_ONE.raw_value);
circle_min = p_offset - r8 + lookup_val.raw_value;
circle_max = p_offset + r8 - lookup_val.raw_value;
} else {
// Bottom part of the circle
Fixed_S16_3 lookup_val = prv_get_circle_border_precise(progress.raw_value -
cap_center.raw_value, cap_radius.raw_value + FIXED_S16_3_ONE.raw_value);
circle_min = p_offset - r8 + lookup_val.raw_value;
circle_max = p_offset + r8 - lookup_val.raw_value;
}
min->raw_value = MIN(min->raw_value, circle_min);
max->raw_value = MAX(max->raw_value, circle_max);
}
}
static void prv_calc_cap_horiz(GPointPrecise *line_end_point, Fixed_S16_3 cap_radius,
int16_t y, Fixed_S16_3 *left_margin, Fixed_S16_3 *right_margin) {
// This function will calculate edges of the cap for stroked line using horizontal lines
Fixed_S16_3 progress = (Fixed_S16_3){.integer = y};
prv_calc_cap_prepared(line_end_point->y, line_end_point->x,
cap_radius, progress, left_margin, right_margin);
}
static void prv_calc_cap_vert(GPointPrecise *line_end_point, Fixed_S16_3 cap_radius,
int16_t x, Fixed_S16_3 *top_margin, Fixed_S16_3 *bottom_margin) {
// This function will calculate edges of the cap for stroked line using vertical lines
Fixed_S16_3 progress = (Fixed_S16_3){.integer = x};
prv_calc_cap_prepared(line_end_point->x, line_end_point->y,
cap_radius, progress, top_margin, bottom_margin);
}
// TODO: test me
static void prv_calc_quadrant_lookup(Fixed_S16_3 lookup[], uint8_t radius) {
int n = ((radius - 1) * radius) / 2;
for (int i=0; i < radius; i++) {
lookup[i].raw_value = s_circle_table[n + i];
}
}
// Finds edge points of the rectangle and returns true if line is vertically dominant
static bool prv_calc_far_points(GPointPrecise *p0, GPointPrecise *p1, Fixed_S16_3 radius,
GPointPrecise *far_top, GPointPrecise *far_bottom,
GPointPrecise *far_left, GPointPrecise *far_right) {
// Increase precision for square root function so we wont lose results when p0 and p1
// are closer to each other than 1px on screen
const int64_t fixed_precision = 4;
// Delta for the orthogonal vector - its rotated by 90 degrees so we swap x/y
// Those values are multiplied by sqrt_precision which later would be removed in line 297/298
const int64_t dx_fixed = ((*p1).y.raw_value - (*p0).y.raw_value) * fixed_precision;
const int64_t dy_fixed = ((*p0).x.raw_value - (*p1).x.raw_value) * fixed_precision;
// Length of the line for orthogonal vector normalization
const int32_t length_fixed = integer_sqrt(dx_fixed * dx_fixed + dy_fixed * dy_fixed);
if (length_fixed == 0) {
// In this case we skip middle part of the stroke to avoid division by zero
GPointPrecise point;
point.x.raw_value = (*p0).x.raw_value;
point.y.raw_value = (*p0).y.raw_value;
(*far_top) = point;
(*far_bottom) = point;
(*far_left) = point;
(*far_right) = point;
return false;
}
// Orthogonal vector for offset points
GPointPrecise v1;
v1.x.raw_value = (dx_fixed * radius.raw_value) / length_fixed;
v1.y.raw_value = (dy_fixed * radius.raw_value) / length_fixed;
// Calculate main body offset points
GPointPrecise points[4];
points[0].x.raw_value = (*p0).x.raw_value + v1.x.raw_value;
points[0].y.raw_value = (*p0).y.raw_value + v1.y.raw_value;
points[1].x.raw_value = (*p0).x.raw_value - v1.x.raw_value;
points[1].y.raw_value = (*p0).y.raw_value - v1.y.raw_value;
points[2].x.raw_value = (*p1).x.raw_value + v1.x.raw_value;
points[2].y.raw_value = (*p1).y.raw_value + v1.y.raw_value;
points[3].x.raw_value = (*p1).x.raw_value - v1.x.raw_value;
points[3].y.raw_value = (*p1).y.raw_value - v1.y.raw_value;
/* Finding out positions fo the points relatively to main body rectangle
* Hardcoded approach since this is faster than extra logic for edge cases
*
* Example case:
*
* . far_top
* \
* /\
* / ' far_right
* far_left . /
* \/
* \
* ' far_bottom
*/
if (dx_fixed > 0) {
if (dy_fixed > 0) {
// Line heading down left
(*far_top) = points[1];
(*far_bottom) = points[2];
(*far_left) = points[3];
(*far_right) = points[0];
} else {
// Line heading down right
(*far_top) = points[0];
(*far_bottom) = points[3];
(*far_left) = points[1];
(*far_right) = points[2];
}
} else {
if (dy_fixed > 0) {
// Line heading up left
(*far_top) = points[3];
(*far_bottom) = points[0];
(*far_left) = points[2];
(*far_right) = points[1];
} else {
// Line heading up right
(*far_top) = points[2];
(*far_bottom) = points[1];
(*far_left) = points[0];
(*far_right) = points[3];
}
}
// Since we already rotated the vector by 90 degrees, delta x is actually delta y
// therefore if x is bigger than y we have vertical dominance
if (ABS(dx_fixed) > ABS(dy_fixed)) {
return true;
}
return false;
}
void prv_draw_stroked_line_precise(GContext* ctx, GPointPrecise p0, GPointPrecise p1,
uint8_t width) {
// This function will draw thick line on the screen using following technique:
// - calculate offset points of the line
// - calculate margin for the round caps at the end of the line
// - proceed to fill stroke line by 1px lines vertically or horizontally based on steepness
// + find the right/top most edge by checking caps and offset points
// + find the left/bottom most edge by checking caps and offset points
// + draw line between left/top most edge and right/bottom most edge
// This algorithm doesn't handle width smaller than 2
PBL_ASSERTN(width >= MINIMUM_PRECISE_STROKE_WIDTH);
Fixed_S16_3 radius = (Fixed_S16_3){.raw_value = ((width - 1) * FIXED_S16_3_ONE.raw_value) / 2};
// Check if the line is in fact point and lies exactly on the pixel
if (p0.x.raw_value == p1.x.raw_value && p0.y.raw_value == p1.y.raw_value &&
p0.x.fraction == 0 && p0.y.fraction == 0) {
// Color hack
const GColor temp_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
// If so, draw a circle with corresponding radius
graphics_fill_circle(ctx, GPoint(p0.x.integer, p0.y.integer), radius.integer);
// Finish color hack
ctx->draw_state.fill_color = temp_color;
// Return without drawing the line since its not necessary
return;
}
GPointPrecise far_top;
GPointPrecise far_bottom;
GPointPrecise far_left;
GPointPrecise far_right;
bool vertical = prv_calc_far_points(&p0, &p1, radius,
&far_top, &far_bottom,
&far_left, &far_right);
// To compensate for rounding errors we need to add half of the precision in specific places
// - we add on top if line is leaning backward
// - we add on bottom if line is leaning forward
// - for lines with perfect horizontal or vertical lines this fix doesnt matter
// same applies to same starting/ending points
bool delta_x_is_positive = ((p1.x.raw_value - p0.x.raw_value) >= 0);
bool delta_y_is_positive = ((p1.y.raw_value - p0.y.raw_value) >= 0);
bool add_on_top = (delta_x_is_positive == delta_y_is_positive);
uint8_t add_top = (add_on_top)? (FIXED_S16_3_ONE.raw_value / 2) : 0;
uint8_t add_bottom = (!add_on_top)? (FIXED_S16_3_ONE.raw_value / 2) : 0;
const int8_t fraction_mask = 0x7;
if (vertical) {
// Left and right most point helpers for main loop
GPointPrecise lm_p0 = far_top;
GPointPrecise lm_p1 = far_left;
GPointPrecise rm_p0 = far_top;
GPointPrecise rm_p1 = far_right;
const int16_t top_point = MIN(p0.y.raw_value, p1.y.raw_value) - radius.raw_value;
const int16_t bottom_point = MAX(p0.y.raw_value, p1.y.raw_value) + radius.raw_value;
const int8_t fraction_for_top = top_point & fraction_mask;
const int8_t fraction_for_bottom = bottom_point & fraction_mask;
// Drawing loop: Iterates over horizontal lines
// As part of optimisation, this algorithm is moving between drawing boundaries,
// so drawing box has to be subtracted from its clipping extremes
const int16_t clip_min_y = ctx->draw_state.clip_box.origin.y
- ctx->draw_state.drawing_box.origin.y;
const int16_t clip_max_y = clip_min_y + ctx->draw_state.clip_box.size.h;
const int16_t y_min = CLIP(top_point >> FIXED_S16_3_PRECISION, clip_min_y, clip_max_y);
const int16_t y_max = CLIP(bottom_point >> FIXED_S16_3_PRECISION, clip_min_y, clip_max_y);
// Blending of first line
if (fraction_for_top != 0) {
int16_t y = y_min;
if (y > lm_p1.y.integer) {
// We're crossing far_left point, time to swap...
lm_p0 = far_left;
lm_p1 = far_bottom;
}
if (y > rm_p1.y.integer) {
// We're crossing far_right point, time to swap...
rm_p0 = far_right;
rm_p1 = far_bottom;
}
// Starting and ending point of the line, initialized with extremes
Fixed_S16_3 left_margin = {.raw_value = INT16_MAX};
Fixed_S16_3 right_margin = {.raw_value = INT16_MIN};
// Find edges for upper cap
GPointPrecise top_point_tmp = (p0.y.raw_value < p1.y.raw_value) ? p0 : p1;
Fixed_S16_3 progress_line = (Fixed_S16_3){.raw_value = (y * FIXED_S16_3_ONE.raw_value +
FIXED_S16_3_ONE.raw_value / 2)};
prv_calc_cap_prepared(top_point_tmp.y, top_point_tmp.x, radius,
progress_line, &left_margin, &right_margin);
// Finally draw line
if (left_margin.raw_value <= right_margin.raw_value) {
graphics_private_plot_horizontal_line(ctx, y, left_margin, right_margin,
(fraction_for_top >> 1));
}
}
for (int16_t y = (fraction_for_top ? y_min + 1 : y_min); y <= y_max; y++) {
if (y > lm_p1.y.integer) {
// We're crossing far_left point, time to swap...
lm_p0 = far_left;
lm_p1 = far_bottom;
}
if (y > rm_p1.y.integer) {
// We're crossing far_right point, time to swap...
rm_p0 = far_right;
rm_p1 = far_bottom;
}
// Starting and ending point of the line, initialized with extremes
Fixed_S16_3 left_margin = {.raw_value = INT16_MAX};
Fixed_S16_3 right_margin = {.raw_value = INT16_MIN};
// Find edges of the line's straight part
if (y >= far_top.y.integer && y <= far_bottom.y.integer) {
// TODO: possible performance optimization: PBL-14744
// TODO: ^^ also possible avoid of following logic to avoid division by zero
// Main part of the stroked line
if (lm_p1.y.raw_value != lm_p0.y.raw_value) {
left_margin.raw_value = lm_p0.x.raw_value + ((lm_p1.x.raw_value - lm_p0.x.raw_value)
* (y - ((lm_p0.y.raw_value + add_top) / FIXED_S16_3_ONE.raw_value)))
* FIXED_S16_3_ONE.raw_value / (lm_p1.y.raw_value - lm_p0.y.raw_value);
} else {
left_margin.raw_value = lm_p0.x.raw_value;
}
if (rm_p1.y.raw_value != rm_p0.y.raw_value) {
right_margin.raw_value = rm_p0.x.raw_value + ((rm_p1.x.raw_value - rm_p0.x.raw_value)
* (y - ((rm_p0.y.raw_value + add_bottom) / FIXED_S16_3_ONE.raw_value)))
* FIXED_S16_3_ONE.raw_value / (rm_p1.y.raw_value - rm_p0.y.raw_value);
} else {
right_margin.raw_value = rm_p0.x.raw_value;
}
}
// Find edges for both caps
prv_calc_cap_horiz(&p0, radius, y, &left_margin, &right_margin);
prv_calc_cap_horiz(&p1, radius, y, &left_margin, &right_margin);
// Finally draw line
if (left_margin.raw_value <= right_margin.raw_value) {
graphics_private_draw_horizontal_line(ctx, y, left_margin, right_margin);
}
}
// Blending of last line
if (fraction_for_bottom != 0) {
int16_t y = y_max + 1;
// Starting and ending point of the line, initialized with extremes
Fixed_S16_3 left_margin = {.raw_value = INT16_MAX};
Fixed_S16_3 right_margin = {.raw_value = INT16_MIN};
// Find edges for bottom cap
GPointPrecise bottom_point_tmp = (p0.y.raw_value > p1.y.raw_value) ? p0 : p1;
Fixed_S16_3 progress_line = (Fixed_S16_3){.raw_value = (y * FIXED_S16_3_ONE.raw_value -
FIXED_S16_3_ONE.raw_value / 2)};
prv_calc_cap_prepared(bottom_point_tmp.y, bottom_point_tmp.x, radius,
progress_line, &left_margin, &right_margin);
// Finally draw line
if (left_margin.raw_value <= right_margin.raw_value) {
graphics_private_plot_horizontal_line(ctx, y, left_margin, right_margin,
(fraction_for_bottom >> 1));
}
}
} else {
// PBL-14798: refactor this.
// Top and bottom most point helpers for main loop
GPointPrecise tm_p0 = far_left;
GPointPrecise tm_p1 = far_top;
GPointPrecise bm_p0 = far_left;
GPointPrecise bm_p1 = far_bottom;
const int8_t fraction_for_left = (MIN(p0.x.raw_value, p1.x.raw_value) - radius.raw_value)
& fraction_mask;
const int8_t fraction_for_right = (MAX(p0.x.raw_value, p1.x.raw_value) + radius.raw_value)
& fraction_mask;
// Drawing loop: Iterates over vertical lines from left to right
// As part of optimisation, this algorithm is moving between drawing boundaries,
// so drawing box has to be subtracted from its clipping extremes
const int16_t clip_min_x = ctx->draw_state.clip_box.origin.x
- ctx->draw_state.drawing_box.origin.x;
const int16_t clip_max_x = clip_min_x + ctx->draw_state.clip_box.size.w;
const int16_t x_min = CLIP((MIN(p0.x.raw_value, p1.x.raw_value) - radius.raw_value)
>> FIXED_S16_3_PRECISION, clip_min_x, clip_max_x);
const int16_t x_max = CLIP((MAX(p0.x.raw_value, p1.x.raw_value) + radius.raw_value)
>> FIXED_S16_3_PRECISION, clip_min_x, clip_max_x);
// Blending of first line
if (fraction_for_left != 0) {
int16_t x = x_min;
if (x > tm_p1.x.integer) {
// We're crossing far_top point, time to swap...
tm_p0 = far_top;
tm_p1 = far_right;
}
if (x > bm_p1.x.integer) {
// We're crossing far_bottom point, time to swap...
bm_p0 = far_bottom;
bm_p1 = far_right;
}
// Starting and ending point of the line, initialized with extremes
Fixed_S16_3 top_margin = {.raw_value = INT16_MAX};
Fixed_S16_3 bottom_margin = {.raw_value = INT16_MIN};
// Find edges for left cap
GPointPrecise left_point_tmp = (p0.y.raw_value < p1.y.raw_value) ? p0 : p1;
Fixed_S16_3 progress_line = (Fixed_S16_3){.raw_value = (x * FIXED_S16_3_ONE.raw_value +
FIXED_S16_3_ONE.raw_value / 2)};
prv_calc_cap_prepared(left_point_tmp.x, left_point_tmp.y, radius,
progress_line, &top_margin, &bottom_margin);
// Finally draw line
if (top_margin.raw_value <= bottom_margin.raw_value) {
graphics_private_plot_vertical_line(ctx, x, top_margin, bottom_margin,
(fraction_for_left >> 1));
}
}
for (int16_t x = (fraction_for_left ? x_min + 1 : x_min); x <= x_max; x++) {
if (x > tm_p1.x.integer) {
// We're crossing far_top point, time to swap...
tm_p0 = far_top;
tm_p1 = far_right;
}
if (x > bm_p1.x.integer) {
// We're crossing far_bottom point, time to swap...
bm_p0 = far_bottom;
bm_p1 = far_right;
}
// Starting and ending point of the line, initialized with extremes
Fixed_S16_3 top_margin = {.raw_value = INT16_MAX};
Fixed_S16_3 bottom_margin = {.raw_value = INT16_MIN};
// Find edges of the line's straight part
if (x >= far_left.x.integer && x <= far_right.x.integer) {
// Main part of the stroked line
if (tm_p1.x.raw_value != tm_p0.x.raw_value) {
top_margin.raw_value = tm_p0.y.raw_value + ((tm_p1.y.raw_value - tm_p0.y.raw_value)
* (x - ((tm_p0.x.raw_value + add_top) / FIXED_S16_3_ONE.raw_value)))
* FIXED_S16_3_ONE.raw_value / (tm_p1.x.raw_value - tm_p0.x.raw_value);
} else {
top_margin.raw_value = tm_p0.y.raw_value;
}
if (bm_p1.x.raw_value != bm_p0.x.raw_value) {
bottom_margin.raw_value =
bm_p0.y.raw_value + ((bm_p1.y.raw_value - bm_p0.y.raw_value)
* (x - ((bm_p0.x.raw_value + add_bottom) / FIXED_S16_3_ONE.raw_value)))
* FIXED_S16_3_ONE.raw_value / (bm_p1.x.raw_value - bm_p0.x.raw_value);
} else {
bottom_margin.raw_value = bm_p0.y.raw_value;
}
}
// Find edges for both caps
prv_calc_cap_vert(&p0, radius, x, &top_margin, &bottom_margin);
prv_calc_cap_vert(&p1, radius, x, &top_margin, &bottom_margin);
// Finally draw line
if (top_margin.raw_value <= bottom_margin.raw_value) {
graphics_private_draw_vertical_line(ctx, x, top_margin, bottom_margin);
}
}
// Blending of last line
if (fraction_for_right != 0) {
int16_t x = x_max + 1;
// Starting and ending point of the line, initialized with extremes
Fixed_S16_3 top_margin = {.raw_value = INT16_MAX};
Fixed_S16_3 bottom_margin = {.raw_value = INT16_MIN};
// Find edges for right cap
GPointPrecise right_point_tmp = (p0.x.raw_value > p1.x.raw_value) ? p0 : p1;
Fixed_S16_3 progress_line = (Fixed_S16_3){.raw_value = (x * FIXED_S16_3_ONE.raw_value -
FIXED_S16_3_ONE.raw_value / 2)};
prv_calc_cap_prepared(right_point_tmp.x, right_point_tmp.y, radius,
progress_line, &top_margin, &bottom_margin);
// Finally draw line
if (top_margin.raw_value <= bottom_margin.raw_value) {
graphics_private_plot_vertical_line(ctx, x, top_margin, bottom_margin,
(fraction_for_right >> 1));
}
}
}
}
static void prv_adjust_stroked_line_width(uint8_t *width) {
PBL_ASSERTN(*width >= MINIMUM_PRECISE_STROKE_WIDTH);
if (*width % 2 == 0) {
(*width)++;
}
}
static void prv_draw_stroked_line_override_aa(GContext* ctx, GPointPrecise p0, GPointPrecise p1,
uint8_t width, bool anti_aliased) {
#if PBL_COLOR
// Force antialiasing setting
bool temp_anti_aliased = ctx->draw_state.antialiased;
ctx->draw_state.antialiased = anti_aliased;
#endif
// Call graphics line draw function
prv_draw_stroked_line_precise(ctx, p0, p1, width);
#if PBL_COLOR
// Restore previous antialiasing setting
ctx->draw_state.antialiased = temp_anti_aliased;
#endif
}
#if PBL_COLOR
MOCKABLE void graphics_line_draw_stroked_aa(GContext* ctx, GPoint p0, GPoint p1,
uint8_t stroke_width) {
prv_adjust_stroked_line_width(&stroke_width);
prv_draw_stroked_line_override_aa(ctx, GPointPreciseFromGPoint(p0), GPointPreciseFromGPoint(p1),
stroke_width, true);
}
#endif // PBL_COLOR
MOCKABLE void graphics_line_draw_stroked_non_aa(GContext* ctx, GPoint p0, GPoint p1,
uint8_t stroke_width) {
prv_adjust_stroked_line_width(&stroke_width);
prv_draw_stroked_line_override_aa(ctx, GPointPreciseFromGPoint(p0), GPointPreciseFromGPoint(p1),
stroke_width, false);
}
#if PBL_COLOR
MOCKABLE void graphics_line_draw_precise_stroked_aa(GContext* ctx, GPointPrecise p0,
GPointPrecise p1, uint8_t stroke_width) {
prv_draw_stroked_line_override_aa(ctx, p0, p1, stroke_width, true);
}
#endif // PBL_COLOR
MOCKABLE void graphics_line_draw_precise_stroked_non_aa(GContext* ctx, GPointPrecise p0,
GPointPrecise p1, uint8_t stroke_width) {
prv_draw_stroked_line_override_aa(ctx, p0, p1, stroke_width, false);
}
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {
if (ctx->draw_state.stroke_width >= MINIMUM_PRECISE_STROKE_WIDTH) {
prv_draw_stroked_line_precise(ctx, p0, p1, ctx->draw_state.stroke_width);
} else {
graphics_draw_line(ctx, GPointFromGPointPrecise(p0), GPointFromGPointPrecise(p1));
}
}
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
if (ctx->draw_state.stroke_width > 1) {
// Antialiased and Stroke Width > 1
graphics_line_draw_stroked_aa(ctx, p0, p1, ctx->draw_state.stroke_width);
return;
} else {
// Antialiased and Stroke Width == 1 (not supported on 1-bit color)
graphics_line_draw_1px_aa(ctx, p0, p1);
return;
}
}
#endif
if (ctx->draw_state.stroke_width > 1) {
// Non-Antialiased and Stroke Width > 1
graphics_line_draw_stroked_non_aa(ctx, p0, p1, ctx->draw_state.stroke_width);
} else {
// Non-Antialiased and Stroke Width == 1
graphics_line_draw_1px_non_aa(ctx, p0, p1);
}
}
static void prv_draw_dotted_line(GContext* ctx, GPoint p0, uint16_t length, bool vertical) {
PBL_ASSERTN(ctx);
if (ctx->lock || (length == 0)) {
return;
}
// Even columns start at pixel 0, odd columns start at pixel 1
// 0 1 2 3 4 5
// 0 X X X
// 1 X X X
// 2 X X X
// 3 X X X
// 4 X X X
// 5 X X X
// absolute coordinate
GPoint abs_point = gpoint_add(p0, ctx->draw_state.drawing_box.origin);
// is first pixel even?
bool even = (abs_point.x + abs_point.y) % 2 == 0;
// direction to travel
const GPoint delta = vertical ? GPoint(0, 1) : GPoint(1, 0);
while (length >= 1) {
if (even) {
graphics_private_set_pixel(ctx, abs_point);
}
even = !even;
gpoint_add_eq(&abs_point, delta);
length--;
}
}
void graphics_draw_vertical_line_dotted(GContext* ctx, GPoint p0, uint16_t length) {
prv_draw_dotted_line(ctx, p0, length, true);
}
void graphics_draw_horizontal_line_dotted(GContext* ctx, GPoint p0, uint16_t length) {
prv_draw_dotted_line(ctx, p0, length, false);
}