Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,194 @@
/*
* 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 "../bitblt_private.h"
#include "applib/app_logging.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/graphics_private.h"
#include "applib/graphics/gtypes.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/graphics.h"
#include "util/size.h"
#define MAX_SUPPORTED_PALETTE_ENTRIES 4
// stores transparent masks + color patterns for even+odd scanlines
typedef struct {
// true, if color entry will be visible on 1bit, false otherwise
bool transparent_mask[MAX_SUPPORTED_PALETTE_ENTRIES];
// a 32bit value you can OR into the 1bit destination for each color entry
uint32_t palette_pattern[MAX_SUPPORTED_PALETTE_ENTRIES];
} RowLookUp;
typedef RowLookUp TwoRowLookUp[2];
T_STATIC void prv_apply_tint_color(GColor *color, GColor tint_color) {
// tint_color.a is always 0 or 3
if (tint_color.a != 0) {
tint_color.a = (*color).a;
*color = tint_color;
}
}
T_STATIC void prv_calc_two_row_look_ups(TwoRowLookUp *look_up,
GCompOp compositing_mode,
const GColor8 *palette,
uint8_t num_entries,
GColor tint_color) {
for (unsigned int palette_index = 0; palette_index < num_entries; palette_index++) {
GColor color = palette[palette_index];
// gcolor_get_grayscale will convert any color with an alpha less than 2 to clear
// alpha should be ignored in the case of GCompOpAssign so the alpha is set to 3
if (compositing_mode == GCompOpAssign) {
color.a = 3;
} else if (compositing_mode == GCompOpTint) {
prv_apply_tint_color(&color, tint_color);
} else if (compositing_mode == GCompOpTintLuminance) {
color = gcolor_tint_using_luminance_and_multiply_alpha(color, tint_color);
}
color = gcolor_get_grayscale(color);
for (unsigned int row_number = 0; row_number < ARRAY_LENGTH(*look_up); row_number++) {
(*look_up)[row_number].palette_pattern[palette_index] =
graphics_private_get_1bit_grayscale_pattern(color, row_number);
(*look_up)[row_number].transparent_mask[palette_index] =
gcolor_is_transparent(color) ? false : true;
}
}
}
void bitblt_bitmap_into_bitmap_tiled_palette_to_1bit(GBitmap* dest_bitmap,
const GBitmap* src_bitmap, GRect dest_rect,
GPoint src_origin_offset,
GCompOp compositing_mode,
GColor tint_color) {
if (!src_bitmap->palette) {
return;
}
const int8_t dest_begin_x = (dest_rect.origin.x / 32);
const uint32_t * const dest_block_x_begin = ((uint32_t *)dest_bitmap->addr) + dest_begin_x;
const int dest_row_length_words = (dest_bitmap->row_size_bytes / 4);
// The number of bits between the beginning of dest_block and
// the beginning of the nearest 32-bit block:
const uint8_t dest_shift_at_line_begin = (dest_rect.origin.x % 32);
const int16_t src_begin_x = src_bitmap->bounds.origin.x;
const int16_t src_begin_y = src_bitmap->bounds.origin.y;
// The bounds size is relative to the bounds origin, but the offset is within the origin. This
// means that the end coordinates may need to be adjusted.
const int16_t src_end_x = grect_get_max_x(&src_bitmap->bounds);
const int16_t src_end_y = grect_get_max_y(&src_bitmap->bounds);
// how many 32-bit blocks do we need to bitblt on this row:
const int16_t dest_end_x = grect_get_max_x(&dest_rect);
const int16_t dest_y_end = grect_get_max_y(&dest_rect);
const uint8_t num_dest_blocks_per_row = (dest_end_x / 32) +
((dest_end_x % 32) ? 1 : 0) - dest_begin_x;
const GColor *palette = src_bitmap->palette;
const uint8_t *src = src_bitmap->addr;
const uint8_t src_bpp = gbitmap_get_bits_per_pixel(gbitmap_get_format(src_bitmap));
const uint8_t src_palette_size = 1 << src_bpp;
PBL_ASSERTN(src_palette_size <= MAX_SUPPORTED_PALETTE_ENTRIES);
// The bitblt loops:
int16_t src_y = src_begin_y + src_origin_offset.y;
int16_t dest_y = dest_rect.origin.y;
TwoRowLookUp look_ups;
prv_calc_two_row_look_ups(&look_ups, compositing_mode, palette, src_palette_size, tint_color);
while (dest_y < dest_y_end) {
if (src_y >= src_end_y) {
src_y = src_begin_y;
}
uint8_t dest_shift = dest_shift_at_line_begin;
RowLookUp look_up = look_ups[dest_y % 2];
int16_t src_x = src_begin_x + src_origin_offset.x;
uint8_t row_bits_left = dest_rect.size.w;
uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length_words);
const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row;
while (dest_block != dest_block_end) {
uint8_t dest_x = dest_shift;
while (dest_x < 32 && row_bits_left > 0) {
if (src_x >= src_end_x) { // Wrap horizontally
src_x = src_begin_x;
}
uint8_t cindex = raw_image_get_value_for_bitdepth(src, src_x, src_y,
src_bitmap->row_size_bytes, src_bpp);
uint32_t mask = 0;
bitset32_update(&mask, dest_x, look_up.transparent_mask[cindex]);
// This can be optimized by performing actions on the current word all at once
// instead of iterating through each pixel
switch (compositing_mode) {
case GCompOpAssign:
case GCompOpSet:
case GCompOpTint:
case GCompOpTintLuminance:
*dest_block = (*dest_block & ~mask) | (look_up.palette_pattern[cindex] & mask);
break;
default:
PBL_LOG(LOG_LEVEL_DEBUG,
"Only the assign, set and tint modes are allowed for palettized bitmaps");
return;
}
dest_x++;
row_bits_left--;
src_x++;
}
dest_shift = 0;
dest_block++;
}
dest_y++;
src_y++;
}
}
void bitblt_bitmap_into_bitmap_tiled(GBitmap* dest_bitmap, const GBitmap* src_bitmap,
GRect dest_rect, GPoint src_origin_offset,
GCompOp compositing_mode, GColor8 tint_color) {
if (bitblt_compositing_mode_is_noop(compositing_mode, tint_color)) {
return;
}
GBitmapFormat src_fmt = gbitmap_get_format(src_bitmap);
GBitmapFormat dest_fmt = gbitmap_get_format(dest_bitmap);
if (dest_fmt != GBitmapFormat1Bit) {
return;
}
switch (src_fmt) {
case GBitmapFormat1Bit:
bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode, tint_color);
break;
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
bitblt_bitmap_into_bitmap_tiled_palette_to_1bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
default:
APP_LOG(APP_LOG_LEVEL_DEBUG, "Only 1 and 2 bit palettized images can be displayed.");
return;
}
}

View file

@ -0,0 +1,61 @@
/*
* 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 "applib/graphics/framebuffer.h"
#include "applib/graphics/gtypes.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/bitset.h"
#include <stdint.h>
#include <string.h>
volatile const int FrameBuffer_MaxX = DISP_COLS;
volatile const int FrameBuffer_MaxY = DISP_ROWS;
volatile const int FrameBuffer_BytesPerRow = FRAMEBUFFER_BYTES_PER_ROW;
uint32_t *framebuffer_get_line(FrameBuffer *f, uint8_t y) {
PBL_ASSERTN(y < f->size.h);
return f->buffer + (y * ((f->size.w / 32) + 1));
}
inline size_t framebuffer_get_size_bytes(FrameBuffer *f) {
// TODO: Make FRAMEBUFFER_SIZE_BYTES a macro which takes the cols and rows if we ever want to
// support different size framebuffers for watches which have native 1-bit framebuffers where the
// size is not just COLS * ROWS.
return FRAMEBUFFER_SIZE_BYTES;
}
void framebuffer_clear(FrameBuffer *f) {
memset(f->buffer, 0xff, framebuffer_get_size_bytes(f));
framebuffer_dirty_all(f);
f->is_dirty = true;
}
void framebuffer_mark_dirty_rect(FrameBuffer *f, GRect rect) {
if (!f->is_dirty) {
f->dirty_rect = rect;
} else {
f->dirty_rect = grect_union(&f->dirty_rect, &rect);
}
const GRect clip_rect = (GRect) { GPointZero, f->size };
grect_clip(&f->dirty_rect, &clip_rect);
f->is_dirty = true;
}

View file

@ -0,0 +1,32 @@
/*
* 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.
*/
#pragma once
#define FRAMEBUFFER_WORDS_PER_ROW ((DISP_COLS / 32) + 1)
#define FRAMEBUFFER_SIZE_DWORDS (DISP_ROWS * FRAMEBUFFER_WORDS_PER_ROW)
#define FRAMEBUFFER_BYTES_PER_ROW (FRAMEBUFFER_WORDS_PER_ROW * 4)
#define FRAMEBUFFER_SIZE_BYTES (DISP_ROWS * FRAMEBUFFER_BYTES_PER_ROW)
typedef struct FrameBuffer {
uint32_t buffer[FRAMEBUFFER_SIZE_DWORDS];
GSize size;
GRect dirty_rect; //<! Smallest rect covering all dirty pixels.
bool is_dirty;
} FrameBuffer;
uint32_t* framebuffer_get_line(FrameBuffer* f, uint8_t y);

View file

@ -0,0 +1,472 @@
/*
* 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 "../bitblt_private.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/profiler.h"
#include "util/graphics.h"
#include "util/bitset.h"
#include "util/math.h"
#if !defined(__clang__)
#pragma GCC optimize("O2")
#endif
// Size is based on color palette
#define LOOKUP_TABLE_SIZE 64
// Blending lookup table to map from:
// dd: 2-bit dest luminance dd,
// ss: src luminance ss,
// aa: src alpha
// to a final 2-bit luminance
// result = s_blending_mask_lookup[0b00aaddss]
// or s_blending_mask_lookup[(aa << 4) | (dd << 2) | ss]
const GColor8Component g_bitblt_private_blending_mask_lookup[LOOKUP_TABLE_SIZE] = {
0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
0, 0, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2, 3, 3,
0, 1, 1, 2, 0, 1, 2, 2, 1, 1, 2, 3, 1, 2, 2, 3,
0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3,
};
void bitblt_bitmap_into_bitmap_tiled_palette_to_8bit(GBitmap* dest_bitmap,
const GBitmap* src_bitmap,
GRect dest_rect,
GPoint src_origin_offset,
GCompOp compositing_mode,
GColor8 tint_color) {
if (!src_bitmap->palette) {
return;
}
// Initialize the tint luminance lookup table if necessary
GColor8 tint_luminance_lookup_table[GCOLOR8_COMPONENT_NUM_VALUES] = {};
if (compositing_mode == GCompOpTintLuminance) {
gcolor_tint_luminance_lookup_table_init(tint_color, tint_luminance_lookup_table);
}
const int16_t dest_begin_y = dest_rect.origin.y;
const int16_t dest_end_y = grect_get_max_y(&dest_rect);
const int16_t src_begin_y = src_bitmap->bounds.origin.y;
const int16_t src_end_y = grect_get_max_y(&src_bitmap->bounds);
const uint8_t src_bpp = gbitmap_get_bits_per_pixel(gbitmap_get_format(src_bitmap));
const GColor *palette = src_bitmap->palette;
int16_t src_y = src_begin_y + src_origin_offset.y;
for (int16_t dest_y = dest_begin_y; dest_y < dest_end_y; ++dest_y, ++src_y) {
// Wrap-around source bitmap vertically
if (src_y >= src_end_y) {
src_y = src_begin_y;
}
const GBitmapDataRowInfo dest_row_info = gbitmap_get_data_row_info(dest_bitmap, dest_y);
uint8_t *dest = dest_row_info.data;
const int16_t dest_delta_begin_x = MAX(dest_row_info.min_x - dest_rect.origin.x, 0);
const int16_t dest_begin_x = dest_delta_begin_x ? dest_row_info.min_x : dest_rect.origin.x;
const int16_t dest_end_x = MIN(grect_get_max_x(&dest_rect), dest_row_info.max_x + 1);
if (dest_end_x < dest_begin_x) {
continue;
}
const GBitmapDataRowInfo src_row_info = gbitmap_get_data_row_info(src_bitmap, src_y);
const uint8_t *src = src_row_info.data;
// This is the initial position that takes into account destination delta shift
const int16_t src_initial_x = src_bitmap->bounds.origin.x + dest_delta_begin_x;
const int16_t src_begin_x = MAX(src_row_info.min_x, src_bitmap->bounds.origin.x);
const int16_t src_end_x = MIN(grect_get_max_x(&src_bitmap->bounds),
src_row_info.max_x + 1);
int16_t src_x = src_initial_x + src_origin_offset.x;
for (int16_t dest_x = dest_begin_x; dest_x < dest_end_x; ++dest_x, ++src_x) {
if (!WITHIN(src_x, src_begin_x, src_end_x - 1)) {
// Check if content should wrap (under and over) for tiling
if (!WITHIN(src_x, src_bitmap->bounds.origin.x,
grect_get_max_x(&src_bitmap->bounds) - 1)) {
// keep correct bounds alignment for circular when tiling
src_x = src_bitmap->bounds.origin.x +
((src_x - src_bitmap->bounds.origin.x) % src_bitmap->bounds.size.w);
} else {
// Increment source but don't draw
continue;
}
}
// src points to the info for the row, so y and stride are 0 for raw_image_get_value
uint8_t cindex = raw_image_get_value_for_bitdepth(src, src_x, 0, 0, src_bpp);
GColor src_color = palette[cindex];
GColor dest_color = (GColor) dest[dest_x];
switch (compositing_mode) {
case GCompOpAssign:
dest[dest_x] = src_color.argb;
break;
case GCompOpSet:
dest[dest_x] = gcolor_alpha_blend(src_color, dest_color).argb;
break;
case GCompOpTint: {
GColor actual_color = tint_color;
actual_color.a = src_color.a;
dest[dest_x] = gcolor_alpha_blend(actual_color, dest_color).argb;
break;
}
case GCompOpTintLuminance: {
const GColor actual_color =
gcolor_perform_lookup_using_color_luminance_and_multiply_alpha(
src_color, tint_luminance_lookup_table);
dest[dest_x] = gcolor_alpha_blend(actual_color, dest_color).argb;
break;
}
default:
PBL_LOG(LOG_LEVEL_DEBUG, "OP: %d NYI", (int)compositing_mode);
return;
}
}
}
}
void bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(GBitmap *dest_bitmap,
const GBitmap *src_bitmap,
GRect dest_rect,
GPoint src_origin_offset,
GCompOp compositing_mode,
GColor8 tint_color) {
const int16_t dest_begin_y = dest_rect.origin.y;
const int16_t dest_end_y = grect_get_max_y(&dest_rect);
const int16_t src_begin_y = src_bitmap->bounds.origin.y;
const int16_t src_end_y = grect_get_max_y(&src_bitmap->bounds);
int16_t src_y = src_begin_y + src_origin_offset.y;
// Default all compositing modes to GCompAssign except for GCompOpSet
// and GCompOpOr.
switch (compositing_mode) {
case GCompOpAssign:
case GCompOpAssignInverted:
case GCompOpAnd:
case GCompOpOr:
case GCompOpClear: {
for (int16_t dest_y = dest_begin_y; dest_y < dest_end_y; ++dest_y, ++src_y) {
// Wrap-around source bitmap vertically
if (src_y >= src_end_y) {
src_y = src_begin_y;
}
const GBitmapDataRowInfo dest_row_info = gbitmap_get_data_row_info(dest_bitmap, dest_y);
uint8_t *dest = dest_row_info.data;
const int16_t dest_delta_begin_x = MAX(dest_row_info.min_x - dest_rect.origin.x, 0);
const int16_t dest_begin_x = dest_delta_begin_x ? dest_row_info.min_x : dest_rect.origin.x;
const int16_t dest_end_x = MIN(grect_get_max_x(&dest_rect), dest_row_info.max_x + 1);
if (dest_end_x < dest_begin_x) {
continue;
}
const GBitmapDataRowInfo src_row_info = gbitmap_get_data_row_info(src_bitmap, src_y);
const uint8_t *src = src_row_info.data;
// This is the initial position that takes into account destination delta shift
const int16_t src_initial_x = src_bitmap->bounds.origin.x + dest_delta_begin_x;
const int16_t src_begin_x = MAX(src_row_info.min_x, src_bitmap->bounds.origin.x);
const int16_t src_end_x = MIN(grect_get_max_x(&src_bitmap->bounds),
src_row_info.max_x + 1);
int16_t src_x = src_initial_x + src_origin_offset.x;
for (int16_t dest_x = dest_begin_x; dest_x < dest_end_x; ++dest_x, ++src_x) {
if (!WITHIN(src_x, src_begin_x, src_end_x - 1)) {
// Check if content should wrap (under and over) for tiling
if (!WITHIN(src_x, src_bitmap->bounds.origin.x,
grect_get_max_x(&src_bitmap->bounds) - 1)) {
// keep correct bounds alignment for circular when tiling
src_x = src_bitmap->bounds.origin.x +
((src_x - src_bitmap->bounds.origin.x) % src_bitmap->bounds.size.w);
} else {
// Increment source but don't draw
continue;
}
}
dest[dest_x] = src[src_x];
}
}
break;
}
case GCompOpTint:
case GCompOpTintLuminance:
case GCompOpSet:
default: {
// Initialize the tint luminance lookup table if necessary
GColor8 tint_luminance_lookup_table[GCOLOR8_COMPONENT_NUM_VALUES] = {};
if (compositing_mode == GCompOpTintLuminance) {
gcolor_tint_luminance_lookup_table_init(tint_color, tint_luminance_lookup_table);
}
for (int16_t dest_y = dest_begin_y; dest_y < dest_end_y; ++dest_y, ++src_y) {
// Wrap-around source bitmap vertically
if (src_y >= src_end_y) {
src_y = src_begin_y;
}
const GBitmapDataRowInfo dest_row_info = gbitmap_get_data_row_info(dest_bitmap, dest_y);
uint8_t *dest = dest_row_info.data;
const int16_t dest_delta_begin_x = MAX(dest_row_info.min_x - dest_rect.origin.x, 0);
const int16_t dest_begin_x = dest_delta_begin_x ? dest_row_info.min_x : dest_rect.origin.x;
const int16_t dest_end_x = MIN(grect_get_max_x(&dest_rect), dest_row_info.max_x + 1);
if (dest_end_x < dest_begin_x) {
continue;
}
const GBitmapDataRowInfo src_row_info = gbitmap_get_data_row_info(src_bitmap, src_y);
const uint8_t *src = src_row_info.data;
// This is the initial position that takes into account destination delta shift
const int16_t src_initial_x = src_bitmap->bounds.origin.x + dest_delta_begin_x;
const int16_t src_begin_x = MAX(src_row_info.min_x, src_bitmap->bounds.origin.x);
const int16_t src_end_x = MIN(grect_get_max_x(&src_bitmap->bounds),
src_row_info.max_x + 1);
int16_t src_x = src_initial_x + src_origin_offset.x;
for (int16_t dest_x = dest_begin_x; dest_x < dest_end_x; ++dest_x, ++src_x) {
if (!WITHIN(src_x, src_begin_x, src_end_x - 1)) {
// Check if content should wrap (under and over) for tiling
if (!WITHIN(src_x, src_bitmap->bounds.origin.x,
grect_get_max_x(&src_bitmap->bounds) - 1)) {
// keep correct bounds alignment for circular when tiling
src_x = src_bitmap->bounds.origin.x +
(((src_x - src_bitmap->bounds.origin.x)) % src_bitmap->bounds.size.w);
} else {
// Increment source but don't draw
continue;
}
}
GColor src_color = *(GColor8 *) &src[src_x];
GColor actual_color = src_color;
if (compositing_mode == GCompOpTint) {
actual_color = tint_color;
actual_color.a = src_color.a;
} else if (compositing_mode == GCompOpTintLuminance) {
actual_color = gcolor_perform_lookup_using_color_luminance_and_multiply_alpha(
src_color, tint_luminance_lookup_table);
}
dest[dest_x] = gcolor_alpha_blend(actual_color, (GColor8)dest[dest_x]).argb;
}
}
break;
}
}
}
void bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(GBitmap* dest_bitmap,
const GBitmap* src_bitmap,
GRect dest_rect,
GPoint src_origin_offset,
GCompOp compositing_mode,
GColor8 tint_color) {
const int16_t dest_begin_y = dest_rect.origin.y;
const int16_t dest_end_y = dest_begin_y + dest_rect.size.h;
int16_t src_y = src_bitmap->bounds.origin.y + src_origin_offset.y;
for (int16_t dest_y = dest_begin_y; dest_y < dest_end_y; ++dest_y, ++src_y) {
// Wrap-around source bitmap vertically:
if (src_y >= src_bitmap->bounds.origin.y + src_bitmap->bounds.size.h) {
src_y = src_bitmap->bounds.origin.y;
}
const GBitmapDataRowInfo dest_row_info = gbitmap_get_data_row_info(dest_bitmap, dest_y);
uint8_t *dest = dest_row_info.data;
const int16_t dest_delta_begin_x = MAX(dest_row_info.min_x - dest_rect.origin.x, 0);
const int16_t dest_begin_x = dest_delta_begin_x ? dest_row_info.min_x : dest_rect.origin.x;
const int16_t dest_end_x = MIN(grect_get_max_x(&dest_rect), dest_row_info.max_x + 1);
if (dest_end_x < dest_begin_x) {
continue;
}
const int16_t corrected_src_x =
src_bitmap->bounds.origin.x + src_origin_offset.x + dest_delta_begin_x;
const uint32_t * const src_block_x_begin =
((uint32_t *)src_bitmap->addr) + corrected_src_x / 32;
const int src_row_length_words = (src_bitmap->row_size_bytes / 4);
const uint8_t src_line_start_idx = corrected_src_x % 32;
const uint8_t src_line_wrap_idx = (src_bitmap->bounds.origin.x + dest_delta_begin_x) % 32;
const uint8_t src_line_start_end_idx =
MIN(32, src_bitmap->bounds.size.w + src_line_start_idx - (src_origin_offset.x % 32));
const uint8_t src_line_wrap_end_idx = MIN(32, src_bitmap->bounds.size.w + src_line_wrap_idx);
uint8_t row_bits_left = dest_rect.size.w;
uint32_t * const src_block_begin =
(uint32_t *)src_block_x_begin + (src_y * src_row_length_words);
uint32_t *src_block = src_block_begin;
uint32_t src = *src_block;
uint8_t src_start_idx = src_line_start_idx;
uint8_t src_end_idx = MIN(src_line_start_end_idx, src_line_start_idx + row_bits_left);
if (src_start_idx > src_end_idx) {
continue;
}
const uint32_t *src_block_end = src_block_begin + src_row_length_words;
int16_t dest_x = dest_begin_x;
while (dest_x < dest_end_x) {
const uint8_t number_of_bits = src_end_idx - src_start_idx;
PBL_ASSERTN(number_of_bits <= row_bits_left);
switch (compositing_mode) {
case GCompOpClear:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
if (set) {
dest[dest_x] = GColorBlack.argb;
}
}
break;
case GCompOpSet:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
if (!set) {
dest[dest_x] = GColorWhite.argb;
}
}
break;
case GCompOpOr:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
if (set) {
dest[dest_x] = GColorWhite.argb;
}
}
break;
case GCompOpAnd:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
if (!set) {
dest[dest_x] = GColorBlack.argb;
}
}
break;
case GCompOpAssignInverted:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
dest[dest_x] = (set) ? GColorBlack.argb : GColorWhite.argb;
}
break;
case GCompOpTint:
case GCompOpTintLuminance:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
if (!set) {
dest[dest_x] = tint_color.argb;
}
}
break;
default:
case GCompOpAssign:
for (int i = src_start_idx; i < src_end_idx; ++i, ++dest_x) {
const uint32_t bit = (1 << i);
const bool set = src & bit;
dest[dest_x] = (set) ? GColorWhite.argb : GColorBlack.argb;
}
break;
}
row_bits_left -= number_of_bits;
if (row_bits_left != 0) {
++src_block;
if (src_block == src_block_end) {
// Wrap-around source bitmap horizontally:
src_block = src_block_begin;
src_start_idx = src_line_wrap_idx;
src_end_idx = MIN(src_line_wrap_end_idx, src_start_idx + row_bits_left);
} else {
src_start_idx = 0;
src_end_idx = MIN(32, row_bits_left);
}
src = *src_block;
}
}
}
}
void bitblt_bitmap_into_bitmap_tiled(GBitmap* dest_bitmap, const GBitmap* src_bitmap,
GRect dest_rect, GPoint src_origin_offset,
GCompOp compositing_mode, GColor tint_color) {
if (bitblt_compositing_mode_is_noop(compositing_mode, tint_color)) {
return;
}
GBitmapFormat src_fmt = gbitmap_get_format(src_bitmap);
// Don't use gbitmap_get_format on dest_bitmap since it's always of known origin.
// In the case of a Legacy2 app, we have a 1-bit src going into an 8-bit dest and do not
// want to override the destination's format
GBitmapFormat dest_fmt = dest_bitmap->info.format;
if (src_fmt == dest_fmt) {
switch (src_fmt) {
case GBitmapFormat1Bit:
bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
case GBitmapFormat8Bit:
case GBitmapFormat8BitCircular:
bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
default:
break;
}
} else {
if (dest_fmt == GBitmapFormat8Bit || dest_fmt == GBitmapFormat8BitCircular) {
switch (src_fmt) {
case GBitmapFormat1Bit:
bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
case GBitmapFormat4BitPalette:
bitblt_bitmap_into_bitmap_tiled_palette_to_8bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
// Circular buffer can take this path
case GBitmapFormat8Bit:
case GBitmapFormat8BitCircular:
bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(dest_bitmap, src_bitmap, dest_rect,
src_origin_offset, compositing_mode,
tint_color);
break;
default:
break;
}
} else {
PBL_LOG(LOG_LEVEL_DEBUG, "Only blitting to 8-bit supported.");
}
}
}

View file

@ -0,0 +1,76 @@
/*
* 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 "applib/graphics/framebuffer.h"
#include "applib/graphics/gtypes.h"
#include "board/display.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/profiler.h"
#include "util/bitset.h"
#include <stdint.h>
#include <string.h>
volatile const int FrameBuffer_MaxX = DISP_COLS;
volatile const int FrameBuffer_MaxY = DISP_ROWS;
volatile const int FrameBuffer_BytesPerRow = FRAMEBUFFER_BYTES_PER_ROW;
uint8_t *framebuffer_get_line(FrameBuffer *f, uint8_t y) {
PBL_ASSERTN(!gsize_equal(&f->size, &GSizeZero));
PBL_ASSERTN(y < f->size.h);
#if PLATFORM_SPALDING
const GBitmapDataRowInfoInternal *row_infos = g_gbitmap_spalding_data_row_infos;
const size_t offset = row_infos[y].offset;
#else
const size_t offset = y * f->size.w;
#endif
return f->buffer + offset;
}
inline size_t framebuffer_get_size_bytes(FrameBuffer *f) {
PBL_ASSERTN(!gsize_equal(&f->size, &GSizeZero));
// TODO: Make FRAMEBUFFER_SIZE_BYTES a macro which takes the cols and rows if we ever want
// to support different size framebuffers for round displays or other displays where the 8-bit
// framebuffer size is not just COLS * ROWS.
#if PLATFORM_SPALDING
return FRAMEBUFFER_SIZE_BYTES;
#else
return (size_t)f->size.w * (size_t)f->size.h;
#endif
}
void framebuffer_clear(FrameBuffer *f) {
PBL_ASSERTN(!gsize_equal(&f->size, &GSizeZero));
memset(f->buffer, 0xff, framebuffer_get_size_bytes(f));
framebuffer_dirty_all(f);
}
void framebuffer_mark_dirty_rect(FrameBuffer *f, GRect rect) {
PBL_ASSERTN(!gsize_equal(&f->size, &GSizeZero));
if (!f->is_dirty) {
f->dirty_rect = rect;
} else {
f->dirty_rect = grect_union(&f->dirty_rect, &rect);
}
const GRect clip_rect = (GRect) { GPointZero, f->size };
grect_clip(&f->dirty_rect, &clip_rect);
f->is_dirty = true;
}

View file

@ -0,0 +1,47 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "drivers/display/display.h"
#include "util/attributes.h"
#include <stdint.h>
#include <stdbool.h>
#define FRAMEBUFFER_BYTES_PER_ROW DISP_COLS
#define FRAMEBUFFER_SIZE_BYTES DISPLAY_FRAMEBUFFER_BYTES
#ifndef UNITTEST
typedef struct FrameBuffer {
uint8_t buffer[FRAMEBUFFER_SIZE_BYTES];
GSize size; //<! Active size of the framebuffer
GRect dirty_rect; //<! Smallest rect covering all dirty pixels.
bool is_dirty;
} FrameBuffer;
#else // UNITTEST
// For unit-tests, the framebuffer buffer is moved to the end of the struct
// and packed to allow for DUMA to catch memory overflows
typedef struct PACKED FrameBuffer {
GSize size; //<! Active size of the framebuffer
GRect dirty_rect; //<! Smallest rect covering all dirty pixels.
bool is_dirty;
uint8_t buffer[FRAMEBUFFER_SIZE_BYTES];
} FrameBuffer;
#endif
uint8_t* framebuffer_get_line(FrameBuffer* f, uint8_t y);

View file

@ -0,0 +1,52 @@
/*
* 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.
*/
// TODO PBL-1744: Load bitmap resource from flash...
static const uint8_t pug[] = {
0x08, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x3e, 0x00, 0xff, 0xff, 0xfd, 0xff, /* bytes 0 - 16 */
0xff, 0xff, 0xff, 0x0f, 0x3f, 0x00, 0xf0, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x07, 0x43, 0xc0, 0xff, /* bytes 16 - 32 */
0xff, 0xff, 0xff, 0x0f, 0x01, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x80, 0x61, 0x00, 0xff, /* bytes 32 - 48 */
0xff, 0xff, 0xff, 0x0f, 0x61, 0x00, 0x03, 0xfe, 0xff, 0xff, 0xff, 0x0f, 0x01, 0x02, 0x00, 0xfe, /* bytes 48 - 64 */
0xff, 0xff, 0xff, 0x0f, 0x11, 0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0x0f, 0x01, 0x00, 0x00, 0xff, /* bytes 64 - 80 */
0xff, 0xff, 0xff, 0x0f, 0x01, 0x00, 0x00, 0xfe, 0xff, 0x01, 0xff, 0x0f, 0x03, 0x00, 0x00, 0xfe, /* bytes 80 - 96 */
0xff, 0x4e, 0xfe, 0x0f, 0x03, 0x00, 0x48, 0xf9, 0x7f, 0x86, 0xfc, 0x0f, 0x03, 0x00, 0x18, 0xf3, /* bytes 96 - 112 */
0x7f, 0x03, 0xf9, 0x0f, 0x07, 0x00, 0x10, 0x02, 0x3e, 0x4b, 0xfb, 0x0f, 0x07, 0x00, 0x00, 0x0b, /* bytes 112 - 128 */
0x00, 0x78, 0xf8, 0x0f, 0x07, 0x00, 0x60, 0xa6, 0xcb, 0x00, 0xfa, 0x0f, 0x07, 0x00, 0x80, 0x25, /* bytes 128 - 144 */
0x36, 0x07, 0xfa, 0x0f, 0x0f, 0x00, 0x20, 0x67, 0x34, 0xf9, 0xf3, 0x0f, 0x1f, 0x00, 0x98, 0x49, /* bytes 144 - 160 */
0xcb, 0xb2, 0xf7, 0x0f, 0x1f, 0x00, 0xc0, 0x4e, 0xda, 0xa6, 0xf6, 0x0f, 0xdf, 0x00, 0x61, 0xd3, /* bytes 160 - 176 */
0xd4, 0xec, 0xe6, 0x0f, 0xdf, 0x70, 0x36, 0xcf, 0xac, 0x9b, 0xef, 0x0f, 0x5f, 0xc7, 0xd8, 0x4e, /* bytes 176 - 192 */
0xae, 0x53, 0xfb, 0x0f, 0x5f, 0x4c, 0xdb, 0xd9, 0xba, 0x4d, 0xdb, 0x0f, 0x9f, 0x79, 0xb5, 0xd7, /* bytes 192 - 208 */
0xda, 0x6d, 0xf6, 0x0f, 0xbf, 0x9b, 0xad, 0x76, 0x76, 0xb7, 0xff, 0x0f, 0xbf, 0xe6, 0xfa, 0x6d, /* bytes 208 - 224 */
0xb6, 0x93, 0x3c, 0x0f, 0xbf, 0x34, 0x5b, 0xdb, 0xee, 0x6e, 0x77, 0x0f, 0xbf, 0xd5, 0x6d, 0x5b, /* bytes 224 - 240 */
0xd9, 0x6d, 0xdb, 0x0f, 0xbf, 0x4d, 0xb7, 0x6d, 0x37, 0x93, 0x6d, 0x0f, 0xbf, 0x79, 0xdb, 0x3d, /* bytes 240 - 256 */
0xed, 0xb6, 0xfd, 0x0f, 0x3f, 0xb7, 0x6c, 0x33, 0xdb, 0x6d, 0xb6, 0x0e, 0xbf, 0xed, 0xf7, 0x96, /* bytes 256 - 272 */
0x36, 0x21, 0xfb, 0x0e, 0xbf, 0x4e, 0xdb, 0x9e, 0x75, 0xc0, 0xf9, 0x0e, 0xbf, 0x7b, 0xbd, 0xcb, /* bytes 272 - 288 */
0x1d, 0x00, 0xef, 0x0e, 0x7f, 0xc7, 0x6c, 0x4a, 0x0a, 0x3e, 0xcb, 0x0d, 0x7f, 0x8d, 0xf3, 0x6d, /* bytes 288 - 304 */
0x83, 0x6d, 0xb6, 0x0d, 0x7f, 0x3a, 0xd2, 0xcd, 0x80, 0x6c, 0xb6, 0x0f, 0xff, 0x1a, 0x26, 0x0f, /* bytes 304 - 320 */
0x9c, 0x1b, 0x6c, 0x0e, 0xff, 0x66, 0xc8, 0x99, 0x3f, 0xf3, 0xed, 0x0c, 0xff, 0x6c, 0xc2, 0xe5, /* bytes 320 - 336 */
0x3f, 0xff, 0xd9, 0x0d, 0xff, 0x4c, 0x4e, 0xef, 0x3f, 0xd9, 0x73, 0x0f, 0xff, 0x5c, 0x9e, 0xf9, /* bytes 336 - 352 */
0x7f, 0xd7, 0xb7, 0x0d, 0xff, 0x55, 0x9e, 0xf6, 0x7f, 0xd6, 0xef, 0x0d, 0xff, 0x6d, 0xbe, 0xdb, /* bytes 352 - 368 */
0xff, 0xdc, 0x6f, 0x09, 0xff, 0x2d, 0x3f, 0xeb, 0xff, 0xbc, 0x5f, 0x0b, 0xff, 0x5b, 0xbf, 0xec, /* bytes 368 - 384 */
0xff, 0xa5, 0xdf, 0x0b, 0xff, 0x59, 0xbf, 0xff, 0xff, 0xd5, 0xbf, 0x0a, 0xff, 0xd9, 0x3f, 0xf9, /* bytes 384 - 400 */
0xff, 0xd9, 0xbf, 0x09, 0xff, 0x6b, 0xbf, 0xed, 0xff, 0xdc, 0xbf, 0x09, 0xff, 0x6b, 0xbf, 0xee, /* bytes 400 - 416 */
0xff, 0xdc, 0x3f, 0x0b, 0xff, 0xdb, 0xbf, 0xfa, 0xff, 0xec, 0x3f, 0x0b, 0xff, 0x33, 0x3f, 0xeb, /* bytes 416 - 432 */
0x7f, 0xea, 0x3f, 0x0b, 0xff, 0x6b, 0xbf, 0xed, 0x3f, 0xc8, 0x3f, 0x03, 0xff, 0x33, 0xbf, 0xfc, /* bytes 432 - 448 */
0x3f, 0xe0, 0x3f, 0x09, 0xff, 0x53, 0x3f, 0xf3, 0x7f, 0xe0, 0x9f, 0x03, 0xff, 0x49, 0x3f, 0xf7, /* bytes 448 - 464 */
0xff, 0xff, 0x1f, 0x02, 0xff, 0x29, 0x9f, 0xe4, 0xff, 0xff, 0x1f, 0x00, 0xff, 0x01, 0x9f, 0xf4, /* bytes 464 - 480 */
0xff, 0xff, 0x3f, 0x08, 0xff, 0x83, 0x1f, 0xe6, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0x0f, 0xf0, /* bytes 480 - 496 */
0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0x1f, 0xf0, 0xff, 0xff, 0xff, 0x0f,
};

View file

@ -0,0 +1,165 @@
/*
* 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 "bitblt.h"
#include "bitblt_private.h"
#include "system/logging.h"
#include "util/math.h"
#include "util/bitset.h"
void bitblt_into_1bit_setup_compositing_mode(GCompOp *compositing_mode,
GColor tint_color) {
if ((*compositing_mode == GCompOpTint) || (*compositing_mode == GCompOpTintLuminance)) {
// Force our interpretation of the tint color to be black, white, or clear
tint_color = gcolor_get_bw(tint_color);
if (gcolor_equal(tint_color, GColorBlack)) {
*compositing_mode = GCompOpAnd;
} else if (gcolor_equal(tint_color, GColorWhite)) {
*compositing_mode = GCompOpSet;
}
}
}
void bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(GBitmap* dest_bitmap,
const GBitmap* src_bitmap, GRect dest_rect,
GPoint src_origin_offset,
GCompOp compositing_mode,
GColor tint_color) {
bitblt_into_1bit_setup_compositing_mode(&compositing_mode, tint_color);
const int8_t dest_begin_x = (dest_rect.origin.x / 32);
const uint32_t * const dest_block_x_begin = ((uint32_t*)dest_bitmap->addr) + dest_begin_x;
const int dest_row_length_words = (dest_bitmap->row_size_bytes / 4);
// The number of bits between the beginning of dest_block and the beginning of the nearest 32-bit block:
const uint8_t dest_shift_at_line_begin = (dest_rect.origin.x % 32);
const uint32_t * const src_block_x_begin =
((uint32_t*)src_bitmap->addr) +
((src_bitmap->bounds.origin.x + (src_origin_offset.x % src_bitmap->bounds.size.w)) / 32);
const int src_row_length_words = (src_bitmap->row_size_bytes / 4);
const uint8_t src_shift_at_line_begin = ((src_bitmap->bounds.origin.x + src_origin_offset.x) % 32);
const uint8_t src_bits_left_at_line_begin = MIN(32 - src_shift_at_line_begin,
MIN(dest_rect.size.w + src_origin_offset.x,
src_bitmap->bounds.size.w));
// how many 32-bit blocks do we need to bitblt on this row:
const int16_t dest_end_x = (dest_rect.origin.x + dest_rect.size.w);
const uint8_t num_dest_blocks_per_row = (dest_end_x / 32) + ((dest_end_x % 32) ? 1 : 0) - dest_begin_x;
// The bitblt loops:
const int16_t dest_y_end = dest_rect.origin.y + dest_rect.size.h;
int16_t src_y = src_bitmap->bounds.origin.y + src_origin_offset.y;
for (int16_t dest_y = dest_rect.origin.y; dest_y != dest_y_end; ++dest_y, ++src_y) {
// Wrap-around source bitmap vertically:
if (src_y >= src_bitmap->bounds.origin.y + src_bitmap->bounds.size.h) {
src_y = src_bitmap->bounds.origin.y;
}
int8_t src_dest_shift = 32 + dest_shift_at_line_begin - src_shift_at_line_begin;
uint8_t dest_shift = dest_shift_at_line_begin;
uint8_t row_bits_left = dest_rect.size.w;
uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length_words);
uint32_t * const src_block_begin = (uint32_t *)src_block_x_begin + (src_y * src_row_length_words);
uint32_t *src_block = src_block_begin;
uint32_t src = *src_block;
rotl32(src, src_dest_shift);
uint8_t src_bits_left = src_bits_left_at_line_begin;
const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row;
const uint32_t *src_block_end = src_block + src_row_length_words;
while (dest_block != dest_block_end) {
const uint8_t number_of_bits = MIN(32 - dest_shift, MIN(row_bits_left, src_bits_left));
const uint32_t mask_outer_bit = ((number_of_bits < 31) ? (1 << number_of_bits) : 0);
const uint32_t mask = ((mask_outer_bit - 1) << dest_shift);
switch (compositing_mode) {
case GCompOpClear:
*(dest_block) &= ~(mask & src);
break;
case GCompOpSet:
*(dest_block) |= mask & ~src;
break;
case GCompOpOr:
*(dest_block) |= mask & src;
break;
case GCompOpAnd:
*(dest_block) &= ~mask | src;
break;
case GCompOpAssignInverted:
*(dest_block) ^= mask & (~src ^ *(dest_block));
break;
default:
case GCompOpAssign:
// this basically does: masked(dest_bits) = masked(src_bits)
*(dest_block) ^= mask & (src ^ *(dest_block));
break;
}
dest_shift = (dest_shift + number_of_bits) % 32;
row_bits_left -= number_of_bits;
src_bits_left -= number_of_bits;
if (src_bits_left == 0 && row_bits_left != 0) {
++src_block;
if (src_block == src_block_end) {
// Wrap-around source bitmap horizontally:
src_block = src_block_begin;
src_bits_left = src_bits_left_at_line_begin;
src_dest_shift = (src_dest_shift + src_bitmap->bounds.size.w) % 32;
} else {
src_bits_left = 32; // excessive right edge bits will be masked off eventually
}
src = *src_block;
rotl32(src, src_dest_shift);
if (dest_shift) {
continue;
}
}
// Proceed to next dest_block:
++dest_block;
}
}
}
void bitblt_bitmap_into_bitmap(GBitmap* dest_bitmap, const GBitmap* src_bitmap,
GPoint dest_offset, GCompOp compositing_mode, GColor8 tint_color) {
GRect dest_rect = { dest_offset, src_bitmap->bounds.size };
grect_clip(&dest_rect, &dest_bitmap->bounds);
GBitmap src_clipped_bitmap = *src_bitmap;
src_clipped_bitmap.bounds.origin = (GPoint) {
src_bitmap->bounds.origin.x + (dest_rect.origin.x - dest_offset.x),
src_bitmap->bounds.origin.y + (dest_rect.origin.y - dest_offset.y)
};
bitblt_bitmap_into_bitmap_tiled(dest_bitmap, &src_clipped_bitmap, dest_rect,
GPointZero, compositing_mode, tint_color);
}
bool bitblt_compositing_mode_is_noop(GCompOp compositing_mode, GColor tint_color) {
return (((compositing_mode == GCompOpTint) || (compositing_mode == GCompOpTintLuminance)) &&
gcolor_is_invisible(tint_color));
}

View file

@ -0,0 +1,28 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
void bitblt_bitmap_into_bitmap_tiled(GBitmap* dest_bitmap, const GBitmap* src_bitmap,
GRect dest_rect, GPoint src_origin_offset,
GCompOp compositing_mode, GColor tint_color);
void bitblt_bitmap_into_bitmap(GBitmap* dest_bitmap, const GBitmap* src_bitmap, GPoint dest_offset,
GCompOp compositing_mode, GColor tint_color);
bool bitblt_compositing_mode_is_noop(GCompOp compositing_mode, GColor tint_color);

View file

@ -0,0 +1,35 @@
/*
* 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 "bitblt.h"
void bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(
GBitmap* dest_bitmap, const GBitmap* src_bitmap, GRect dest_rect,
GPoint src_origin_offset, GCompOp compositing_mode, GColor tint_color);
void bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(
GBitmap* dest_bitmap, const GBitmap* src_bitmap, GRect dest_rect,
GPoint src_origin_offset, GCompOp compositing_mode, GColor8 tint_color);
void bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
GBitmap* dest_bitmap, const GBitmap* src_bitmap, GRect dest_rect,
GPoint src_origin_offset, GCompOp compositing_mode, GColor8 tint_color);
// Used when source bitmap is 1 bit and the destination is 1 or 8 bit.
// Sets up the GCompOp based on the tint_color.
void bitblt_into_1bit_setup_compositing_mode(GCompOp *compositing_mode, GColor tint_color);
extern const GColor8Component g_bitblt_private_blending_mask_lookup[];

View file

@ -0,0 +1,66 @@
/*
* 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.
*/
//! @file framebuffer.c
//! Bitdepth independant routines for framebuffer.h
//! Bitdepth depenedant routines can be found in the 1_bit & 8_bit folders in their
//! respective framebuffer.c files.
#include "applib/graphics/framebuffer.h"
#include "system/passert.h"
void framebuffer_init(FrameBuffer *fb, const GSize *size) {
PBL_ASSERTN(!gsize_equal(size, &GSizeZero));
fb->size = *size;
framebuffer_reset_dirty(fb);
// make sure the size is not bigger than the actual buffer size
PBL_ASSERTN(framebuffer_get_size_bytes(fb) <= FRAMEBUFFER_SIZE_BYTES);
}
GBitmap framebuffer_get_as_bitmap(FrameBuffer *fb, const GSize *size) {
PBL_ASSERTN(!gsize_equal(size, &GSizeZero));
const GBitmapDataRowInfoInternal *data_row_infos =
PBL_IF_RECT_ELSE(NULL, g_gbitmap_spalding_data_row_infos);
return (GBitmap) {
.addr = fb->buffer,
.row_size_bytes = gbitmap_format_get_row_size_bytes(size->w, GBITMAP_NATIVE_FORMAT),
.info = (BitmapInfo) {.format = GBITMAP_NATIVE_FORMAT, .version = GBITMAP_VERSION_CURRENT},
.bounds = (GRect) { GPointZero, *size },
.data_row_infos = data_row_infos,
};
}
void framebuffer_dirty_all(FrameBuffer *fb) {
PBL_ASSERTN(!gsize_equal(&fb->size, &GSizeZero));
fb->dirty_rect = (GRect) { GPointZero, fb->size };
fb->is_dirty = true;
}
void framebuffer_reset_dirty(FrameBuffer *fb) {
PBL_ASSERTN(!gsize_equal(&fb->size, &GSizeZero));
fb->dirty_rect = GRectZero;
fb->is_dirty = false;
}
bool framebuffer_is_dirty(FrameBuffer *fb) {
PBL_ASSERTN(!gsize_equal(&fb->size, &GSizeZero));
return fb->is_dirty;
}
GSize framebuffer_get_size(FrameBuffer *fb) {
return fb->size;
}

View file

@ -0,0 +1,64 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#if SCREEN_COLOR_DEPTH_BITS == 8
#include "applib/graphics/8_bit/framebuffer.h"
#else
#include "applib/graphics/1_bit/framebuffer.h"
#endif
#include <stdint.h>
#include <stdbool.h>
extern volatile const int FrameBuffer_MaxX;
extern volatile const int FrameBuffer_MaxY;
//! Initializes the framebuffer by setting the size.
void framebuffer_init(FrameBuffer *fb, const GSize *size);
//! Get the active buffer size in bytes
size_t framebuffer_get_size_bytes(FrameBuffer *f);
//! Clears the screen buffer.
//! Will not be visible on the display until graphics_flush_frame_buffer is called.
void framebuffer_clear(FrameBuffer* f);
//! Mark the given rect of pixels as dirty
void framebuffer_mark_dirty_rect(FrameBuffer* f, GRect rect);
//! Mark the entire framebuffer as dirty
void framebuffer_dirty_all(FrameBuffer* f);
//! Clear the dirty status for this framebuffer
void framebuffer_reset_dirty(FrameBuffer* f);
//! Query the dirty status for this framebuffer
bool framebuffer_is_dirty(FrameBuffer* f);
//! Creates a GBitmap struct that points to the framebuffer. Useful for using the framebuffer data
//! with graphics routines. Note that updating this bitmap won't mark the appropriate lines as
//! dirty in the framebuffer, so this will have to be done manually.
//! @note The size which is passed in should come from app_manager_get_framebuffer_size() for the
//! app framebuffer (or generated based on DISP_ROWS / DISP_COLS for the system framebuffer) to
//! protect against malicious apps changing their own framebuffer size.
GBitmap framebuffer_get_as_bitmap(FrameBuffer *f, const GSize *size);
//! Get the framebuffer size
GSize framebuffer_get_size(FrameBuffer *f);

View file

@ -0,0 +1,599 @@
/*
* 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 "gtypes.h"
#include "gbitmap_pbi.h"
#include "gbitmap_png.h"
#include "applib/applib_malloc.auto.h"
#include "applib/applib_resource_private.h"
#include "applib/graphics/graphics.h"
#include "process_state/app_state/app_state.h"
#include "system/logging.h"
#include "system/passert.h"
#include "syscall/syscall.h"
#include <string.h>
#include <stddef.h>
uint8_t gbitmap_get_bits_per_pixel(GBitmapFormat format) {
switch (format) {
case GBitmapFormat1Bit:
case GBitmapFormat1BitPalette:
return 1;
case GBitmapFormat2BitPalette:
return 2;
case GBitmapFormat4BitPalette:
return 4;
case GBitmapFormat8Bit:
case GBitmapFormat8BitCircular:
return 8;
}
return 0;
}
//! @return the size in bytes of the palette for a given format
uint8_t gbitmap_get_palette_size(GBitmapFormat format) {
switch (format) {
case GBitmapFormat1Bit:
case GBitmapFormat8Bit:
case GBitmapFormat8BitCircular:
return 0;
default:
return (1 << gbitmap_get_bits_per_pixel(format));
}
return 0;
}
uint16_t gbitmap_format_get_row_size_bytes(int16_t width, GBitmapFormat format) {
switch (format) {
case GBitmapFormat1Bit:
return ((width + 31) / 32 ) * 4; // word aligned bytes
case GBitmapFormat8Bit:
return width;
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
case GBitmapFormat4BitPalette:
return ((width * gbitmap_get_bits_per_pixel(format) + 7) / 8); // byte aligned
case GBitmapFormat8BitCircular:
return 0; // variable width
}
return 0;
}
static GBitmap* prv_allocate_gbitmap(void) {
if (process_manager_compiled_with_legacy2_sdk()) {
return (GBitmap *) applib_type_zalloc(GBitmapLegacy2);
}
return applib_type_zalloc(GBitmap);
}
static size_t prv_gbitmap_size(void) {
if (process_manager_compiled_with_legacy2_sdk()) {
return applib_type_size(GBitmapLegacy2);
}
return applib_type_size(GBitmap);
}
static void prv_init_gbitmap_version(GBitmap *bitmap) {
if (process_manager_compiled_with_legacy2_sdk()) {
bitmap->info.version = GBITMAP_VERSION_0;
}
bitmap->info.version = GBITMAP_VERSION_CURRENT;
}
uint8_t gbitmap_get_version(const GBitmap *bitmap) {
if (process_manager_compiled_with_legacy2_sdk()) {
return GBITMAP_VERSION_0;
}
return bitmap->info.version;
}
// indirection to allow conditional mocking in unit-tests
T_STATIC
#if !UNITTEST
// apparently, GCC doesn't inline this otherwise
// scary, I wonder how many more places like these aren't inlined
ALWAYS_INLINE
#endif
GBitmapDataRowInfo prv_gbitmap_get_data_row_info(const GBitmap *bitmap, uint16_t y) {
if (bitmap->info.format == GBitmapFormat8BitCircular) {
const GBitmapDataRowInfoInternal *info = &bitmap->data_row_infos[y];
return (GBitmapDataRowInfo) {
.data = (uint8_t *)bitmap->addr + info->offset,
.min_x = info->min_x,
.max_x = info->max_x,
};
} else {
return (GBitmapDataRowInfo) {
.data = (uint8_t*)bitmap->addr + y * bitmap->row_size_bytes,
.min_x = 0,
// while this is conceptually wrong for .max_x as it should be
// (.row_size_bytes / .bytes_per_pixel) - 1
// it's still a valid value as we assume grect_get_max_x(.bounds) < .row_size_bytes * bpp
// that way this is an efficient implementation of this functions contract
.max_x = grect_get_max_x(&bitmap->bounds) - 1,
};
}
}
MOCKABLE GBitmapDataRowInfo gbitmap_get_data_row_info(const GBitmap *bitmap, uint16_t y) {
return prv_gbitmap_get_data_row_info(bitmap, y);
}
void gbitmap_init_with_data(GBitmap *bitmap, const uint8_t *data) {
BitmapData* bitmap_data = (BitmapData*) data;
memset(bitmap, 0, prv_gbitmap_size());
bitmap->row_size_bytes = bitmap_data->row_size_bytes;
bitmap->info_flags = bitmap_data->info_flags;
// Force this to false, just in case someone passes us some funny looking data.
bitmap->info.is_bitmap_heap_allocated = false;
// Note that our container contains values for the origin, but we want to ignore them.
// This is because orginally we just serialized GBitmap to disk,
// but these fields don't really make sense for static images.
// These origin fields are only used when reusing a byte buffer in a sub bitmap.
// This allows us to have a shallow copy of a portion of a parent bitmap.
// See gbitmap_init_as_sub_bitmap.
bitmap->bounds.origin.x = 0; //((int16_t*)data)[2];
bitmap->bounds.origin.y = 0; //((int16_t*)data)[3];
bitmap->bounds.size.w = bitmap_data->width;
bitmap->bounds.size.h = bitmap_data->height;
bitmap->info.format = gbitmap_get_format(bitmap);
if (gbitmap_get_palette_size(gbitmap_get_format(bitmap)) > 0) {
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk());
// Palette is positioned right after the pixel data
bitmap->palette = (GColor*)(bitmap_data->data +
(bitmap->row_size_bytes * bitmap->bounds.size.h));
// Don't flag this as heap allocated, as it gets freed along with pixel data
bitmap->info.is_palette_heap_allocated = false;
}
bitmap->addr = bitmap_data->data;
// Anything (not Legacy2) being loaded in this manner is being converted to the latest version.
prv_init_gbitmap_version(bitmap);
}
GBitmap* gbitmap_create_with_data(const uint8_t *data) {
GBitmap* bitmap = prv_allocate_gbitmap();
if (bitmap) {
gbitmap_init_with_data(bitmap, data);
}
return bitmap;
}
void gbitmap_init_as_sub_bitmap(GBitmap *sub_bitmap, const GBitmap *base_bitmap, GRect sub_rect) {
if (gbitmap_get_version(base_bitmap) == GBITMAP_VERSION_0) {
GBitmapLegacy2 *legacy_bitmap = (GBitmapLegacy2 *) sub_bitmap;
*legacy_bitmap = *(GBitmapLegacy2 *) base_bitmap;
// it's the responsibility of the parent bitmap to free the underlying data
legacy_bitmap->is_heap_allocated = false;
} else {
*sub_bitmap = *base_bitmap;
// it's the responsibility of the parent bitmap to free the underlying data and palette
sub_bitmap->info.is_palette_heap_allocated = false;
sub_bitmap->info.is_bitmap_heap_allocated = false;
}
grect_clip(&sub_rect, &base_bitmap->bounds);
sub_bitmap->bounds = sub_rect;
}
GBitmap* gbitmap_create_as_sub_bitmap(const GBitmap *base_bitmap, GRect sub_rect) {
GBitmap *bitmap = prv_allocate_gbitmap();
if (bitmap) {
gbitmap_init_as_sub_bitmap(bitmap, base_bitmap, sub_rect);
}
return bitmap;
}
static GColor* prv_allocate_palette(GBitmapFormat format) {
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk());
GColor *palette = NULL;
uint8_t palette_size = gbitmap_get_palette_size(format);
if (palette_size > 0) {
palette = applib_zalloc(palette_size * sizeof(GColor));
}
return palette;
}
#define BITMAP_FORMAT_IS_CIRCULAR_FULL_SCREEN(size, format) \
((format) == GBitmapFormat8BitCircular && (size).w == DISP_COLS && (size).h == DISP_ROWS)
T_STATIC size_t prv_gbitmap_size_for_data(GSize size, GBitmapFormat format) {
#if PLATFORM_SPALDING
if (BITMAP_FORMAT_IS_CIRCULAR_FULL_SCREEN(size, format)) {
return DISPLAY_FRAMEBUFFER_BYTES;
}
#endif
return gbitmap_format_get_row_size_bytes(size.w, format) * size.h;
}
static bool prv_gbitmap_allocate_data_for_size(GBitmap *bitmap, GSize size, GBitmapFormat format) {
if (!bitmap) {
return false;
}
bitmap->row_size_bytes = gbitmap_format_get_row_size_bytes(size.w, format);
bitmap->bounds.size.w = size.w;
bitmap->bounds.size.h = size.h;
prv_init_gbitmap_version(bitmap);
bitmap->info.format = format;
const size_t data_size = prv_gbitmap_size_for_data(size, format);
bitmap->addr = applib_zalloc(data_size);
if (bitmap->addr) {
bitmap->info.is_bitmap_heap_allocated = true;
return true;
}
return false;
}
static GBitmap* prv_gbitmap_create_blank(GSize size, GBitmapFormat format) {
GBitmap *bitmap = prv_allocate_gbitmap();
if (bitmap) {
if (!prv_gbitmap_allocate_data_for_size(bitmap, size, format)) {
applib_free(bitmap);
return NULL;
}
#ifdef PLATFORM_SPALDING
if (BITMAP_FORMAT_IS_CIRCULAR_FULL_SCREEN(size, format)) {
bitmap->data_row_infos = g_gbitmap_spalding_data_row_infos;
}
#endif
}
return bitmap;
}
static bool prv_platform_supports_format(GSize size, GBitmapFormat format) {
switch (format) {
#if PBL_BW
case GBitmapFormat1Bit:
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
return true;
#elif PBL_COLOR && PBL_RECT
case GBitmapFormat1Bit:
case GBitmapFormat8Bit:
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
case GBitmapFormat4BitPalette:
return true;
#elif PBL_COLOR && PBL_ROUND
case GBitmapFormat1Bit:
case GBitmapFormat8Bit:
case GBitmapFormat1BitPalette:
case GBitmapFormat2BitPalette:
case GBitmapFormat4BitPalette:
return true;
case GBitmapFormat8BitCircular:
return BITMAP_FORMAT_IS_CIRCULAR_FULL_SCREEN(size, format);
#endif
default:
return false;
}
}
static bool prv_is_palettized_format(GBitmapFormat format) {
return format >= GBitmapFormat1BitPalette && format <= GBitmapFormat4BitPalette;
}
T_STATIC GBitmap *prv_gbitmap_create_blank_internal_no_platform_checks(GSize size,
GBitmapFormat format) {
GBitmap* bitmap = prv_gbitmap_create_blank(size, format);
// If bitmap allocated and format requires a palette
if (bitmap && prv_is_palettized_format(format)) {
bitmap->palette = prv_allocate_palette(format);
if (bitmap->palette) {
bitmap->info.is_palette_heap_allocated = true;
} else {
gbitmap_destroy(bitmap);
bitmap = NULL;
}
}
return bitmap;
}
GBitmap* gbitmap_create_blank(GSize size, GBitmapFormat format) {
if (process_manager_compiled_with_legacy2_sdk() && format != GBitmapFormat1Bit) {
return NULL;
}
if (!prv_platform_supports_format(size, format)) {
return NULL;
}
return prv_gbitmap_create_blank_internal_no_platform_checks(size, format);
}
GBitmapLegacy2* gbitmap_create_blank_2bit(GSize size) {
return (GBitmapLegacy2 *) gbitmap_create_blank(size, GBitmapFormat1Bit);
}
GBitmap* gbitmap_create_blank_with_palette(GSize size, GBitmapFormat format,
GColor *palette, bool free_on_destroy) {
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk());
if (!prv_platform_supports_format(size, format)) {
return NULL;
}
if (!prv_is_palettized_format(format)) {
return NULL;
}
GBitmap *bitmap = prv_gbitmap_create_blank(size, format);
if (bitmap) {
gbitmap_set_palette(bitmap, palette, free_on_destroy);
}
return bitmap;
}
// Adapted from http://aggregate.org/MAGIC/#Bit%20Reversal
T_STATIC uint8_t prv_byte_reverse(uint8_t b) {
b = (b & 0xaa) >> 1 | (b & 0x55) << 1;
b = (b & 0xcc) >> 2 | (b & 0x33) << 2;
b = (b & 0xf0) >> 4 | (b & 0x0f) << 4;
return b;
}
GBitmap* gbitmap_create_palettized_from_1bit(const GBitmap *src_bitmap) {
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk());
GBitmap *bitmap = NULL;
if (src_bitmap && gbitmap_get_format(src_bitmap) == GBitmapFormat1Bit) {
// Allocate the full size of the image up until the end of the bounds.
// This eliminates edge cases where the bounds may start within a byte,
// and not enough space would be allocated. This allows us to do all copying
// from { 0, 0 } and simplifies copy.
GSize size = (GSize) {
.w = src_bitmap->bounds.size.w + src_bitmap->bounds.origin.x,
.h = src_bitmap->bounds.size.h + src_bitmap->bounds.origin.y
};
bitmap = gbitmap_create_blank(size, GBitmapFormat1BitPalette);
if (bitmap) {
// Perform conversion
uint8_t *src_data = (uint8_t *)src_bitmap->addr;
uint8_t *dest_data = (uint8_t *)bitmap->addr;
for (int y = 0; y < bitmap->bounds.size.h; ++y) {
for (int b = 0; b < bitmap->row_size_bytes; ++b) {
int dest_idx = y * bitmap->row_size_bytes + b;
int src_idx = y * src_bitmap->row_size_bytes + b;
dest_data[dest_idx] = prv_byte_reverse(src_data[src_idx]);
}
}
bitmap->bounds = src_bitmap->bounds;
bitmap->palette[0] = GColorBlack;
bitmap->palette[1] = GColorWhite;
}
}
return bitmap;
}
bool gbitmap_init_with_resource(GBitmap* bitmap, uint32_t resource_id) {
ResAppNum app_resource_bank = sys_get_current_resource_num();
return gbitmap_init_with_resource_system(bitmap, app_resource_bank, resource_id);
}
GBitmap *gbitmap_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return gbitmap_create_with_resource_system(app_num, resource_id);
}
GBitmap *gbitmap_create_with_resource_system(ResAppNum app_num, uint32_t resource_id) {
GBitmap *bitmap = prv_allocate_gbitmap();
if (!bitmap) {
return NULL;
}
if (!gbitmap_init_with_resource_system(bitmap, app_num, resource_id)) {
applib_free(bitmap);
return NULL;
}
return bitmap;
}
static bool prv_init_with_pbi_data(GBitmap *bitmap, uint8_t *data, size_t data_size,
bool is_builtin) {
// Initialize our metadata
gbitmap_init_with_data(bitmap, data);
if (is_builtin) {
// for builtin resources, we don't do the extra bitmap manipulation below.
return true;
}
// Verify the metadata is valid
const GBitmapFormat format = gbitmap_get_format(bitmap);
const size_t addr_offset = offsetof(BitmapData, data);
const uint32_t pixel_data_bytes = bitmap->row_size_bytes * bitmap->bounds.size.h;
const uint32_t required_total_size_bytes =
addr_offset + // header size
pixel_data_bytes + // pixel data
gbitmap_get_palette_size(format); // palette data
const uint32_t required_row_size_bits =
(bitmap->bounds.size.w * gbitmap_get_bits_per_pixel(format));
// Convert from 8 bits in a byte, taking care to round up to the next whole byte.
const uint32_t required_row_size_bytes = (required_row_size_bits + 7) / 8;
if (data_size != required_total_size_bytes ||
required_row_size_bytes > bitmap->row_size_bytes) {
PBL_LOG(LOG_LEVEL_WARNING, "Bitmap metadata is inconsistent! data_size %u",
(unsigned int) data_size);
PBL_LOG(LOG_LEVEL_WARNING, "format %u row_size_bytes %"PRIu16" width %"PRId16" height %"PRId16,
format, bitmap->row_size_bytes, bitmap->bounds.size.w, bitmap->bounds.size.h);
return false;
}
// Move the actual pixel data up to the front of the buffer.
// This way bitmap->addr points to the start of the buffer and can be directly freed.
memmove(data, data + addr_offset, data_size - addr_offset);
bitmap->addr = data;
bitmap->info.is_bitmap_heap_allocated = true;
// Move where the palette now points to, palette is positioned right after the pixel data
if (gbitmap_get_palette_size(format) > 0) {
bitmap->palette = (GColor*)((uint8_t*)bitmap->addr +
(bitmap->row_size_bytes * bitmap->bounds.size.h));
}
return true;
}
bool gbitmap_init_with_resource_system(GBitmap* bitmap, ResAppNum app_num, uint32_t resource_id) {
if (!bitmap) {
return false;
}
memset(bitmap, 0, prv_gbitmap_size());
const size_t data_size = sys_resource_size(app_num, resource_id);
uint8_t *data = applib_resource_mmap_or_load(app_num, resource_id, 0, data_size, false);
if (!data) {
return false;
}
// Scan the resource data to see if it contains PNG data
if (gbitmap_png_data_is_png(data, data_size)) {
const bool result = gbitmap_init_with_png_data(bitmap, data, data_size);
// the actual pixels live uncompressed on the heap now, we can free the PNG data
applib_resource_munmap_or_free(data);
return result;
}
const bool mmapped = applib_resource_is_mmapped(data);
if (prv_init_with_pbi_data(bitmap, data, data_size, mmapped)) {
// in order to make memory-mapped bitmaps work, we need to decrement the reference counter
// when we destroy it. This case is different from a sub-bitmap that shares the bitmap
// data. We use .is_bitmap_heap_allocated=true here so that bitmap_deinit() can take care
// of it.
// As the pixel data is either memory-mapped or heap-allocated we always say "true"
bitmap->info.is_bitmap_heap_allocated = true;
return true;
} else {
applib_resource_munmap_or_free(data);
return false;
}
}
uint16_t gbitmap_get_bytes_per_row(const GBitmap *bitmap) {
if (!bitmap) {
return 0;
}
return bitmap->row_size_bytes;
}
static bool prv_gbitmap_is_context(const GBitmap *bitmap) {
return (bitmap->addr == graphics_context_get_bitmap(app_state_get_graphics_context())->addr);
}
GBitmapFormat gbitmap_get_format(const GBitmap *bitmap) {
if (!bitmap) {
return GBitmapFormat1Bit;
}
if (process_manager_compiled_with_legacy2_sdk() ||
gbitmap_get_version(bitmap) == GBITMAP_VERSION_0) {
// If the bitmap is from the graphics context, return its format
// otherwise return the Legacy2 default 1-Bit format
// to support legacy applications that mis-set the format flags
return (prv_gbitmap_is_context(bitmap)) ? bitmap->info.format : GBitmapFormat1Bit;
}
return bitmap->info.format;
}
uint8_t* gbitmap_get_data(const GBitmap *bitmap) {
if (!bitmap) {
return NULL;
}
return bitmap->addr;
}
void gbitmap_set_data(GBitmap *bitmap, uint8_t *data, GBitmapFormat format,
uint16_t row_size_bytes, bool free_on_destroy) {
if (bitmap) {
bitmap->addr = data;
bitmap->info.format = format;
bitmap->row_size_bytes = row_size_bytes;
bitmap->info.is_bitmap_heap_allocated = free_on_destroy;
}
}
GColor* gbitmap_get_palette(const GBitmap *bitmap) {
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk());
if (!bitmap) {
return NULL;
}
return bitmap->palette;
}
void gbitmap_set_palette(GBitmap *bitmap, GColor *palette, bool free_on_destroy) {
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk());
if (bitmap && palette) {
if (gbitmap_get_info(bitmap).is_palette_heap_allocated) {
applib_free(bitmap->palette);
}
bitmap->palette = palette;
bitmap->info.is_palette_heap_allocated = free_on_destroy;
}
}
GRect gbitmap_get_bounds(const GBitmap *bitmap) {
if (!bitmap) {
return GRectZero;
}
return bitmap->bounds;
}
void gbitmap_set_bounds(GBitmap *bitmap, GRect bounds) {
if (bitmap) {
bitmap->bounds = bounds;
}
}
void gbitmap_deinit(GBitmap* bitmap) {
if (gbitmap_get_info(bitmap).is_bitmap_heap_allocated) {
applib_resource_munmap_or_free(bitmap->addr);
}
bitmap->addr = NULL;
if (!process_manager_compiled_with_legacy2_sdk()) {
if (gbitmap_get_info(bitmap).is_palette_heap_allocated) {
applib_free(bitmap->palette);
}
bitmap->palette = NULL;
}
}
void gbitmap_destroy(GBitmap* bitmap) {
if (!bitmap) {
return;
}
gbitmap_deinit(bitmap);
applib_free(bitmap);
}

View file

@ -0,0 +1,97 @@
/*
* 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.
*/
#pragma once
//! @addtogroup Foundation
//! @{
//! @addtogroup Resources
//! @{
//! @addtogroup FileFormats File Formats
//! @{
//! @addtogroup PBIFileFormat PBI File Format
//!
//! PBIs are uncompressed bitmap images with support for color-mapping palettes.
//! PBIs store images either as raw image pixels (1-bit black and white, or 8-bit ARGB) or as
//! palette-based images with 1, 2, or 4 bits per pixel.
//! For palette-based images the pixel data represents the index into the palette, such
//! that each pixel only needs to be large enough to represent the palette size, so
//! \li \c 1-bit supports up to 2 colors,
//! \li \c 2-bit supports up to 4 colors,
//! \li \c 4-bit supports up to 16 colors.
//!
//! The metadata describes how long each row of pixels is in the buffer (the stride).
//! The following restrictions on stride are in place for different formats:
//!
//! - \ref GBitmapFormat1Bit:
//! Each row must be a multiple of 32 pixels (4 bytes). Using the `bounds` field,
//! the area that is actually relevant can be specified.
//! For example, when the image is 29 by 5 pixels
//! (width by height) and the first bit of image data is the pixel at (0, 0),
//! then the bounds.size would be `GSize(29, 5)` and bounds.origin would be `GPoint(0, 0)`.
//! ![](gbitmap.png)
//! In the illustration each pixel is a representated as a square. The white
//! squares are the bits that are used, the gray squares are the padding bits, because
//! each row of image data has to be a multiple of 4 bytes (32 bits).
//! The numbers in the column in the left are the offsets (in bytes) from the `*addr`
//! field of the GBitmap.
//! Each pixel in a bitmap is represented by 1 bit. If a bit is set (`1` or `true`),
//! it will result in a white pixel, and vice versa, if a bit is cleared (`0` or `false`),
//! it will result in a black pixel.
//! ![](pixel_bit_values.png)
//!
//! - \ref GBitmapFormat8Bit:
//! Each pixel in the bitmap is represented by 1 byte. The color value of that byte correspends to
//! a GColor.argb value.
//! There is no restriction on row_size_bytes / stride.
//!
//! - \ref GBitmapFormat1BitPalette, \ref GBitmapFormat2BitPalette, \ref GBitmapFormat4BitPalette:
//! Each pixel in the bitmap is represented by the number of bits the format specifies. Pixels
//! must be packed.
//! For example, in GBitmapFormat2BitPalette, each pixel uses 2 bits. This means 4 pixels / byte.
//! Rows need to be byte-aligned, meaning that there can be up to 3 unused pixels at the end of
//! each line. If the image is 5 pixels wide and 4 pixels tall, row_size_bytes = 2,
//! and each row in the bitmap must take 2 bytes, so the bitmap data is 8 bytes in total.
//!
//! Palettized bitmaps also need to have a palette. The palette must be of the correct size, which
//! is specified by the format. For example, \ref GBitmapFormat4BitPalette uses 4 bits per pixel,
//! meaning that there must be 2^4 = 16 colors in the palette.
//!
//! The Basalt Platform provides for 2-bits per color channel, so images are optimized by the
//! SDK tooling when loaded as a resource-type "pbi" to the Pebble's 64-colors with 4 levels
//! of transparency. This optimization also handles mapping unsupported colors to the nearest
//! supported color, and reducing the pixel depth to the number of bits required to support
//! the optimized number of colors.
//!
//! @see \ref gbitmap_create_with_data
//! @see \ref gbitmap_create_with_resource
//!
//! @{
//! @} // end addtogroup pbi_file_format
//! @} // end addtogroup FileFormats
//! @} // end addtogroup Resources
//! @} // end addtogroup Foundation
//! This struct is used to either embed bitmap data directly into the software image or when
//! reading resources from SPI flash.
typedef struct __attribute__((__packed__)) {
uint16_t row_size_bytes;
uint16_t info_flags;
uint16_t deprecated[2];
uint16_t width;
uint16_t height;
uint8_t data[]; // Pixel data followed by an optional palette
} BitmapData;

View file

@ -0,0 +1,293 @@
/*
* 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 "gbitmap_png.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "system/logging.h"
#include "syscall/syscall.h"
#include "util/net.h"
#define PNG_DECODE_ERROR "PNG decoding failed"
#define PNG_MEMORY_ERROR "PNG memory allocation failed"
#define PNG_FORMAT_ERROR "Unsupported PNG format, only PNG8 is supported!"
#define PNG_LOAD_ERROR "Failed to load PNG"
static GBitmapFormat prv_get_format_for_bpp(uint8_t bits_per_pixel) {
if (bits_per_pixel == 1) return GBitmapFormat1BitPalette;
if (bits_per_pixel == 2) return GBitmapFormat2BitPalette;
if (bits_per_pixel == 4) return GBitmapFormat4BitPalette;
return GBitmapFormat8Bit;
}
bool gbitmap_png_data_is_png(const uint8_t *data, size_t data_size) {
if (data_size >= sizeof(PNG_SIGNATURE)) {
// PNG files start with [137, 'P', 'N', 'G']
return (ntohl(*(uint32_t*)data) == PNG_SIGNATURE);
}
return false;
}
// ! Distance from current resource cursor to next IDAT/fdAT chunk including that chunks data
int32_t png_seek_chunk_in_resource(uint32_t resource_id, uint32_t offset,
bool seek_framedata, bool *found_actl) {
ResAppNum app_num = sys_get_current_resource_num();
return png_seek_chunk_in_resource_system(app_num, resource_id, offset, seek_framedata,
found_actl);
}
int32_t png_seek_chunk_in_resource_system(ResAppNum app_num, uint32_t resource_id, uint32_t offset,
bool seek_framedata, bool *found_actl) {
uint32_t current_offset = offset;
bool actl_chunk_found = false; // ACTL chunk indicates PNG is an APNG
struct png_chunk_marker {
uint32_t length;
uint32_t chunk_type;
} marker;
// we are assuming the current_offset is always left at the start of the next chunk
// for alignment purposes
size_t max_size = sys_resource_size(app_num, resource_id);
while (current_offset + sizeof(marker) < max_size) {
if (sizeof(marker) != sys_resource_load_range(app_num, resource_id, current_offset,
(uint8_t*)&marker, sizeof(marker))) {
return -1;
}
// Need to byte swap it
marker.length = ntohl(marker.length);
marker.chunk_type = ntohl(marker.chunk_type);
if (marker.chunk_type == CHUNK_ACTL) {
actl_chunk_found = true;
}
if (seek_framedata) {
if (marker.chunk_type == CHUNK_FDAT || marker.chunk_type == CHUNK_IDAT) {
if (found_actl) {
*found_actl = actl_chunk_found;
}
// current distance + data_length + chunk_parts
return (current_offset - offset + marker.length + CHUNK_META_SIZE);
}
} else { // Seeking for data up to but not including FCTL or IDAT chunk (ie. image metadata)
if (marker.chunk_type == CHUNK_IDAT || marker.chunk_type == CHUNK_FCTL) {
if (found_actl) {
*found_actl = actl_chunk_found;
}
// current distance to the beginning of this chunk
return (current_offset - offset);
}
}
current_offset += CHUNK_META_SIZE + marker.length;
}
return -1; // Error
}
GBitmap* gbitmap_create_from_png_data(const uint8_t *png_data, size_t png_data_size) {
GBitmap *bitmap = applib_type_malloc(GBitmap);
if (bitmap) {
memset(bitmap, 0, sizeof(GBitmap));
gbitmap_init_with_png_data(bitmap, png_data, png_data_size);
}
return bitmap;
}
bool gbitmap_init_with_png_data(GBitmap *bitmap, const uint8_t *data, size_t data_size) {
GColor8 *palette = NULL;
bool retval = false;
upng_t *upng = upng_create();
if (!upng) {
goto cleanup;
}
upng_load_bytes(upng, data, data_size);
upng_error upng_state = upng_decode_image(upng);
if (upng_state != UPNG_EOK) {
APP_LOG(APP_LOG_LEVEL_ERROR, (upng_state == UPNG_ENOMEM) ? PNG_MEMORY_ERROR : PNG_DECODE_ERROR);
goto cleanup;
}
// Use UPNG to decode image and get data
uint32_t width = upng_get_width(upng);
uint32_t height = upng_get_height(upng);
uint8_t *upng_buffer = (uint8_t*)upng_get_buffer(upng);
uint32_t bpp = upng_get_bpp(upng);
uint16_t palette_size = 0;
if (!gbitmap_png_is_format_supported(upng)) {
APP_LOG(APP_LOG_LEVEL_ERROR, PNG_FORMAT_ERROR);
goto cleanup;
}
// Create a color palette in GColor8 format from RGB24 + ALPHA8 PNG Palettes (or Grayscale)
palette_size = gbitmap_png_load_palette(upng, &palette);
if (palette_size == 0) {
goto cleanup;
}
// Get the GBitmap format based on the bit depth of the raw data
GBitmapFormat format = prv_get_format_for_bpp(bpp);
// Convert 8-bit palettized PNGs to raw ARGB color images in-place
// as we don't support palettized bitdepths above 4
if (format == GBitmapFormat8Bit) {
for (uint32_t i = 0; i < width * height; i++) {
upng_buffer[i] = palette[upng_buffer[i]].argb; // De-palettize the image data
}
applib_free(palette); // Free the palette to avoid storing it as part of GBitmap
palette = NULL;
}
// Set the image or pixel data
gbitmap_set_data(bitmap, upng_buffer, format,
gbitmap_format_get_row_size_bytes(width, format), true);
gbitmap_set_bounds(bitmap, (GRect){.origin = {0, 0}, .size = {width, height}});
bitmap->info.version = GBITMAP_VERSION_CURRENT;
if (palette) {
gbitmap_set_palette(bitmap, palette, true);
}
retval = true;
cleanup:
if (!retval) {
// bitmap init failed, free palette
APP_LOG(APP_LOG_LEVEL_ERROR, PNG_LOAD_ERROR);
applib_free(palette);
}
// we are keeping the image data to avoid copying it
upng_destroy(upng, !retval);
return retval;
}
static uint16_t prv_gbitmap_png_create_palette_for_grayscale(upng_t *upng, GColor8 **palette_out) {
uint16_t palette_entries = 0;
uint32_t bpp = upng_get_bpp(upng);
// Convert Luminance format from Grayscale to palette
// Pebble only has 4 grayscale shades + 1 transparent value, max bpp == 4
if (bpp > 4) {
return 0;
}
int32_t transparent_gray = gbitmap_png_get_transparent_gray_value(upng);
// Palette will be size required to hold count of shades of gray
palette_entries = 0x1 << bpp;
GColor8 *palette = (GColor8*)applib_malloc(palette_entries * sizeof(GColor8));
if (!palette) {
return 0;
}
memset(palette, 0, palette_entries * sizeof(GColor8));
for (uint16_t i = 0; i < palette_entries; i ++) {
// If the color value matches transparent_gray, color is transparent
if (transparent_gray >= 0 && i == transparent_gray) {
palette[i] = GColorClear;
} else {
// Only have 2 bits per channel, but attempt to make grayscale 4-bit work
// which occurs with black, white, gray1, gray2 and a transparent color
uint8_t luminance = 0;
if (bpp > 2) {
luminance = (i >> (bpp - 2));
} else if (bpp == 2) {
// For bitdepth 2, use bits directly
luminance = i;
} else if (bpp == 1) {
// For bitdepth 1, need max and minimal values
luminance = i ? 0x3 : 0x0;
}
palette[i] = (GColor8){.a = 0x3, .r = luminance, .g = luminance, .b = luminance};
}
}
// Return the converted palette and number of entries
*palette_out = palette;
return palette_entries;
}
static uint16_t prv_gbitmap_png_create_palette_for_color(upng_t *upng, GColor8 **palette_out) {
if (!palette_out) {
return 0;
}
rgb *rgb_palette = NULL;
uint16_t palette_entries = upng_get_palette(upng, &rgb_palette);
uint8_t *alpha_palette = NULL;
uint16_t alpha_palette_entries = upng_get_alpha_palette(upng, &alpha_palette);
// To make palette entries consistent with PBI, pad to the bitdepth number of colors
uint32_t padded_palette_size = (1 << upng_get_bpp(upng));
GColor8 *palette = (GColor8*)applib_malloc(padded_palette_size * sizeof(GColor8));
if (palette == NULL) {
return 0;
}
memset(palette, 0, padded_palette_size * sizeof(GColor8));
// Convert rgb + alpha palette to GColor8 palette
for (int i = 0; i < palette_entries; i++) {
(palette)[i] = GColorFromRGBA(
rgb_palette[i].r, rgb_palette[i].g, rgb_palette[i].b, // RGB
(i < alpha_palette_entries) ? alpha_palette[i] : UINT8_MAX); // Conditional A value
}
// Return the converted palette and number of entries
*palette_out = palette;
return palette_entries;
}
uint16_t gbitmap_png_load_palette(upng_t *upng, GColor8 **palette_out) {
if (upng) {
upng_format png_format = upng_get_format(upng);
// Create a color palette in RGBA8 format from RGB24 + ALPHA8 PNG Palettes
if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
return prv_gbitmap_png_create_palette_for_color(upng, palette_out);
} else if (png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8) {
return prv_gbitmap_png_create_palette_for_grayscale(upng, palette_out);
}
}
return 0;
}
bool gbitmap_png_is_format_supported(upng_t *upng) {
if (upng) {
upng_format png_format = upng_get_format(upng);
if ((png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) ||
(png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8)) {
return true;
}
}
return false;
}
int32_t gbitmap_png_get_transparent_gray_value(upng_t *upng) {
int32_t transparent_gray = -1; // default to invalid value
// Handle grayscale transparency value (1 single transparent gray)
uint8_t *alpha_palette = NULL;
uint16_t alpha_palette_entries = upng_get_alpha_palette(upng, &alpha_palette);
if (alpha_palette_entries == 2) {
transparent_gray = ntohs(*(uint16_t*)alpha_palette);
}
return transparent_gray;
}

View file

@ -0,0 +1,132 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
#include "upng.h"
#include <stdint.h>
#include <stdbool.h>
//! @addtogroup Foundation
//! @{
//! @addtogroup Resources
//! @{
//! @addtogroup FileFormats File Formats
//! @{
//! @addtogroup PNGFileFormat PNG8 File Format
//!
//! Pebble supports both a PBIs (uncompressed bitmap images) as well as PNG8 images.
//! PNG images are compressed allowing for storage savings up to 90%.
//! PNG8 is a PNG that uses palette-based or grayscale images with 1, 2, 4 or 8 bits per pixel.
//! For palette-based images the pixel data represents the index into the palette, such
//! that each pixel only needs to be large enough to represent the palette size, so
//! \li \c 1-bit supports up to 2 colors,
//! \li \c 2-bit supports up to 4 colors,
//! \li \c 4-bit supports up to 16 colors,
//! \li \c 8-bit supports up to 256 colors.
//!
//! There are 2 parts to the palette: the RGB24 color-mapping palette ("PLTE"), and the optional
//! 8-bit transparency palette ("tRNs"). A pixel's color index maps to both tables, combining to
//! allow the pixel to have both color as well as transparency.
//!
//! For grayscale images, the pixel data represents the luminosity (or shade of gray).
//! \li \c 1-bit supports black and white
//! \li \c 2-bit supports black, dark_gray, light_gray and white
//! \li \c 4-bit supports black, white and 14 shades of gray
//! \li \c 8-bit supports black, white and 254 shades of gray
//!
//! Optionally, grayscale images allow for 1 fully transparent color, which is removed from
//! the fully-opaque colors above (e.g. a 2 bit grayscale image can have black, white, dark_gray
//! and a transparent color).
//!
//! The Basalt Platform provides for 2-bits per color channel, so images are optimized by the
//! SDK tooling when loaded as a resource-type "png" to the Pebble's 64-colors with 4 levels
//! of transparency. This optimization also handles mapping unsupported colors to the nearest
//! supported color, and reducing the pixel depth to the number of bits required to support
//! the optimized number of colors. PNG8 images from other sources are supported, with the colors
//! truncated to match supported colors at runtime.
//!
//! @see \ref gbitmap_create_from_png_data
//! @see \ref gbitmap_create_with_resource
//!
//! @{
//! @} // end addtogroup png_file_format
//! @} // end addtogroup FileFormats
//! @} // end addtogroup Resources
//! @} // end addtogroup Foundation
//! This function scans the data array for the PNG file signature
//! @param data to check for the PNG signature
//! @param data_size size of data array in bytes
//! @return True if the data starts with a PNG file signature
bool gbitmap_png_data_is_png(const uint8_t *data, size_t data_size);
//! @addtogroup Graphics
//! @{
//! @addtogroup GraphicsTypes Graphics Types
//! @{
//! Create a \ref GBitmap based on raw PNG data.
//! The resulting \ref GBitmap must be destroyed using \ref gbitmap_destroy().
//! The developer is responsible for freeing png_data following this call.
//! @note PNG decoding currently supports 1,2,4 and 8 bit palettized and grayscale images.
//! @param png_data PNG image data.
//! @param png_data_size PNG image size in bytes.
//! @return A pointer to the \ref GBitmap. `NULL` if the \ref GBitmap could not
//! be created
GBitmap* gbitmap_create_from_png_data(const uint8_t *png_data, size_t png_data_size);
bool gbitmap_init_with_png_data(GBitmap *bitmap, const uint8_t *data, size_t data_size);
//! @} // end addtogroup GraphicsTypes
//! @} // end addtogroup Graphics
//! This function retrieves a GColor8 color palette from a PNG loaded by uPNG
//! @param upng Pointer to upng containing loaded PNG data
//! @param[out] palette_out Handle to GColor8 palette to allocate and fill with GColor8 palette
//! @return Count of colors in palette, 0 otherwise
uint16_t gbitmap_png_load_palette(upng_t *upng, GColor8 **palette_out);
//! This function retrieves a transparent gray matching value from a PNG loaded by uPNG
//! @param upng Pointer to upng containing loaded PNG data
//! @return Transparent gray value for grayscale PNGs if found, -1 otherwise
int32_t gbitmap_png_get_transparent_gray_value(upng_t *upng);
//! This function checks if the format of the loaded upng header is supported
//! @param upng Pointer to upng containing loaded PNG header
//! @return True if supported, False otherwise
bool gbitmap_png_is_format_supported(upng_t *upng);
//! @internal
int32_t png_seek_chunk_in_resource(uint32_t resource_id, uint32_t offset,
bool seek_framedata, bool *found_actl);
//! @internal
//! This function returns the distance from an offset in a resource, from the specified app number,
//! to next IDAT/fdAT chunk including that chunks data
//! @param app_num the app resource space from which to read the resource
//! @param resource_id Resource to seek for PNG/APNG informational chunks
//! @param offset Position in resource (in bytes) to start seeking from
//! @param seek_framedata Option to seek framedata (FDAT/IDAT)
//! or framedata and frame control (FCTL/IDAT)
//! @param found_actl if not NULL, contains if the actl chunk was encountered during seeking
//! @return If seek_framedata is true, returns offset to FDAT or IDAT chunk
//! including chunk data size, otherwise returns offset to FCTL or IDAT
//! not including those chunks data size
int32_t png_seek_chunk_in_resource_system(ResAppNum app_num, uint32_t resource_id, uint32_t offset,
bool seek_framedata, bool *found_actl);

View file

@ -0,0 +1,492 @@
/*
* 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 "gbitmap_sequence.h"
#include "gbitmap_png.h"
#include "util/graphics.h"
#include "util/net.h"
#include "util/time/time.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/math.h"
#define APNG_DECODE_ERROR "APNG decoding failed"
#define APNG_MEMORY_ERROR "APNG memory allocation failed"
#define APNG_FORMAT_ERROR "Unsupported APNG format, only APNG8 is supported!"
#define APNG_LOAD_ERROR "Failed to load APNG"
#define APNG_UPDATE_ERROR "gbitmap_sequence failed to update bitmap"
#define APNG_ELAPSED_WARNING "invalid elapsed_ms for gbitmap_sequence, forward progression only"
static bool prv_gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence, bool reset_elapsed) {
if (bitmap_sequence == NULL) {
return false;
}
// can start seeking after SIG + IHDR
int32_t metadata_bytes = png_seek_chunk_in_resource(bitmap_sequence->resource_id,
PNG_HEADER_SIZE, false, NULL);
if (metadata_bytes <= 0) {
return false;
}
metadata_bytes += PNG_HEADER_SIZE;
bitmap_sequence->png_decoder_data.read_cursor = metadata_bytes;
bitmap_sequence->current_frame = 0;
bitmap_sequence->current_frame_delay_ms = 0;
if (reset_elapsed) {
bitmap_sequence->elapsed_ms = 0;
bitmap_sequence->play_index = 0;
}
return true;
}
//! Directly modifies dst, blending src into dst using equation
//! dst = src * (alpha_normalized) + dst * (1 - alpha_normalized)
static ALWAYS_INLINE void prv_gbitmap_sequence_blend_over(GColor8 src_color, GColor8 *dst) {
if (src_color.a == 3) {
// Fast path: 100% opacity
*dst = src_color;
} else if (src_color.a == 0) {
// Fast path: 0% opacity, no-op!
} else {
const GColor8 dest_color = *dst;
const uint8_t f_src = src_color.a;
const uint8_t f_dst = 3 - f_src;
GColor8 final = {};
final.r = (src_color.r * f_src + dest_color.r * f_dst) / 3;
final.g = (src_color.g * f_src + dest_color.g * f_dst) / 3;
final.b = (src_color.b * f_src + dest_color.b * f_dst) / 3;
final.a = src_color.a; // Different than bitblt, required for correct transparency
*dst = final;
}
}
GBitmapSequence *gbitmap_sequence_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return gbitmap_sequence_create_with_resource_system(app_num, resource_id);
}
GBitmapSequence *gbitmap_sequence_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id) {
uint8_t *frame_data_buffer = NULL;
// Allocate gbitmap
GBitmapSequence* bitmap_sequence = applib_type_zalloc(GBitmapSequence);
if (bitmap_sequence == NULL) {
goto cleanup;
}
bitmap_sequence->resource_id = resource_id;
bitmap_sequence->data_is_loaded_from_flash = true;
if (!prv_gbitmap_sequence_restart(bitmap_sequence, true)) {
goto cleanup;
}
int32_t frame_bytes = bitmap_sequence->png_decoder_data.read_cursor;
frame_data_buffer = applib_zalloc(frame_bytes);
if (frame_data_buffer == NULL) {
goto cleanup;
}
const size_t bytes_read = sys_resource_load_range(app_num, resource_id,
0, frame_data_buffer, frame_bytes);
if (bytes_read != (size_t)frame_bytes) {
goto cleanup;
}
upng_t *upng = upng_create();
if (upng == NULL) {
goto cleanup;
}
bitmap_sequence->png_decoder_data.upng = upng;
upng_load_bytes(upng, frame_data_buffer, frame_bytes);
upng_error upng_state = upng_decode_metadata(upng);
if (upng_state != UPNG_EOK) {
APP_LOG(APP_LOG_LEVEL_ERROR,
(upng_state == UPNG_ENOMEM) ? APNG_MEMORY_ERROR : APNG_DECODE_ERROR);
goto cleanup;
}
// Save metadata to bitmap_sequence
uint32_t play_count = 0;
// If png is APNG, get num plays, otherwise play count is 0
if (upng_is_apng(upng)) {
play_count = upng_apng_num_plays(upng);
// At the API level 0 is no loops vs APNG specification uses 0 for infinite
play_count = (play_count == 0) ? PLAY_COUNT_INFINITE : play_count;
}
bitmap_sequence->play_count = play_count;
bitmap_sequence->bitmap_size = (GSize){.w = upng_get_width(upng), .h = upng_get_height(upng)};
bitmap_sequence->total_frames = upng_apng_num_frames(upng);
if (!gbitmap_png_is_format_supported(upng)) {
APP_LOG(APP_LOG_LEVEL_ERROR, APNG_FORMAT_ERROR);
goto cleanup;
}
// Create a color palette in RGBA8 format from RGB24 + ALPHA8 PNG Palettes
upng_format png_format = upng_get_format(upng);
if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
bitmap_sequence->png_decoder_data.palette_entries =
gbitmap_png_load_palette(upng, &bitmap_sequence->png_decoder_data.palette);
if (bitmap_sequence->png_decoder_data.palette_entries == 0) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Failed to load palette");
goto cleanup;
}
}
bitmap_sequence->header_loaded = true;
cleanup:
applib_free(frame_data_buffer); // Free compressed image buffer
if (!bitmap_sequence || !bitmap_sequence->header_loaded) {
APP_LOG(APP_LOG_LEVEL_ERROR, APNG_LOAD_ERROR);
gbitmap_sequence_destroy(bitmap_sequence);
}
return bitmap_sequence;
}
bool gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence) {
return prv_gbitmap_sequence_restart(bitmap_sequence, true);
}
void gbitmap_sequence_destroy(GBitmapSequence *bitmap_sequence) {
if (bitmap_sequence) {
upng_destroy(bitmap_sequence->png_decoder_data.upng, true);
applib_free(bitmap_sequence->png_decoder_data.palette);
applib_free(bitmap_sequence);
}
}
static ALWAYS_INLINE GColor8 *prv_target_pixel_addr(GBitmap *bitmap, apng_fctl *fctl,
uint32_t x, uint32_t y) {
uint32_t offset = (fctl->y_offset + y + bitmap->bounds.origin.y) * bitmap->row_size_bytes +
(fctl->x_offset + x + bitmap->bounds.origin.x);
GColor8 *pixel_data = bitmap->addr;
return &pixel_data[offset];
}
static void prv_set_pixel_in_row(uint8_t *row_data, GBitmapFormat bitmap_format,
uint32_t x, GColor8 color) {
if (bitmap_format == GBitmapFormat1Bit) {
if (!gcolor_is_invisible(color)) {
const bool pixel_is_white = !gcolor_equal(color, GColorBlack);
bitset8_update(row_data, x, pixel_is_white);
}
} else if ((bitmap_format == GBitmapFormat8Bit) ||
(bitmap_format == GBitmapFormat8BitCircular)) {
GColor8 *const destination_pixel = (GColor8 *)(row_data + x);
*destination_pixel = color;
} else {
WTF; // Unsupported destination type
}
}
bool gbitmap_sequence_update_bitmap_next_frame(GBitmapSequence *bitmap_sequence,
GBitmap *bitmap, uint32_t *delay_ms) {
bool retval = false;
uint8_t* buffer = NULL;
// Disabled if play count is 0 and not the very first frame
if (!bitmap_sequence ||
(bitmap_sequence->play_count == 0 && bitmap_sequence->current_frame != 0)) {
return false;
}
GBitmapSequencePNGDecoderData *png_decoder_data = &bitmap_sequence->png_decoder_data;
upng_t *upng = png_decoder_data->upng;
// Check bitmap_sequence metadata is loaded, bitmap_sequence size, type & memory constraints
const GBitmapFormat bitmap_format = gbitmap_get_format(bitmap); // call is NULL-safe
if (!bitmap_sequence->header_loaded || bitmap == NULL || bitmap->addr == NULL ||
bitmap_sequence->bitmap_size.w > (bitmap->bounds.size.w) ||
bitmap_sequence->bitmap_size.h > (bitmap->bounds.size.h)) {
goto cleanup;
}
if (!((bitmap_format == GBitmapFormat1Bit) ||
(bitmap_format == GBitmapFormat8Bit) ||
(bitmap_format == GBitmapFormat8BitCircular))) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid destination bitmap format for APNG");
goto cleanup;
}
// Update current time elapsed using the previous frames current_frame_delay_ms
bitmap_sequence->elapsed_ms += bitmap_sequence->current_frame_delay_ms;
// Check if single animation loop is complete, and restart if there are more loops
if (bitmap_sequence->current_frame >= bitmap_sequence->total_frames) {
if ((++bitmap_sequence->play_index < bitmap_sequence->play_count) ||
(bitmap_sequence->play_count == PLAY_COUNT_INFINITE)) {
prv_gbitmap_sequence_restart(bitmap_sequence, false);
} else {
return false; // animation complete
}
}
const int32_t metadata_bytes =
png_seek_chunk_in_resource(bitmap_sequence->resource_id,
png_decoder_data->read_cursor, true, NULL);
if (metadata_bytes <= 0) {
goto cleanup;
}
buffer = applib_zalloc(metadata_bytes);
if (buffer == NULL) {
goto cleanup;
}
ResAppNum app_num = sys_get_current_resource_num();
const size_t bytes_read = sys_resource_load_range(
app_num, bitmap_sequence->resource_id,
png_decoder_data->read_cursor, buffer, metadata_bytes);
if (bytes_read != (size_t)metadata_bytes) {
goto cleanup;
}
png_decoder_data->read_cursor += metadata_bytes;
upng_load_bytes(upng, buffer, metadata_bytes);
upng_error upng_state = upng_decode_image(upng);
if (upng_state != UPNG_EOK) {
APP_LOG(APP_LOG_LEVEL_ERROR,
(upng_state == UPNG_ENOMEM) ? APNG_MEMORY_ERROR : APNG_DECODE_ERROR);
goto cleanup;
}
applib_free(buffer);
bitmap_sequence->current_frame++;
const uint32_t width = bitmap_sequence->bitmap_size.w;
const uint32_t height = bitmap_sequence->bitmap_size.h;
const bool bitmap_supports_transparency = (bitmap_format != GBitmapFormat1Bit);
// DISPOSE_OP_BACKGROUND sets the background to black with transparency (0x00)
// If we don't support tranparency, just do nothing.
if (bitmap_supports_transparency &&
(png_decoder_data->last_dispose_op == APNG_DISPOSE_OP_BACKGROUND)) {
const uint32_t y_origin = bitmap->bounds.origin.y + png_decoder_data->previous_yoffset;
for (uint32_t y = y_origin; y < y_origin + png_decoder_data->previous_height; y++) {
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, y);
const uint32_t x_origin = bitmap->bounds.origin.x + png_decoder_data->previous_xoffset;
const int16_t min_x = MAX((uint32_t)row_info.min_x, x_origin);
const int16_t max_x = MIN((uint32_t)row_info.max_x,
(x_origin + png_decoder_data->previous_width - 1));
const int16_t num_bytes = max_x - min_x + 1;
if (num_bytes > 0) {
memset(row_info.data + min_x, 0, num_bytes);
}
}
}
apng_fctl fctl = {0}; // Defaults work for IDAT frame without fctl data
// If this frame doesn't have fctl, use the full width & height
if (!upng_get_apng_fctl(upng, &fctl)) {
fctl.width = width;
fctl.height = height;
// As a PNG image is only a single frame, display it forever
bitmap_sequence->current_frame_delay_ms = PLAY_DURATION_INFINITE;
} else {
png_decoder_data->last_dispose_op = fctl.dispose_op;
png_decoder_data->previous_xoffset = fctl.x_offset;
png_decoder_data->previous_yoffset = fctl.y_offset;
png_decoder_data->previous_width = fctl.width;
png_decoder_data->previous_height = fctl.height;
fctl.delay_den = (fctl.delay_den == 0) ? APNG_DEFAULT_DELAY_UNITS : fctl.delay_den;
// Update the current_frame_delay_ms for this frame
bitmap_sequence->current_frame_delay_ms =
((uint32_t)fctl.delay_num * MS_PER_SECOND) / fctl.delay_den;
}
// Return the delay_ms for the new frame
if (delay_ms != NULL) {
*delay_ms = bitmap_sequence->current_frame_delay_ms;
}
uint32_t bpp = upng_get_bpp(upng);
upng_format png_format = upng_get_format(upng);
uint8_t *upng_buffer = (uint8_t*)upng_get_buffer(upng);
// Byte aligned rows for image at bpp
uint16_t row_stride_bytes = (fctl.width * bpp + 7) / 8;
if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
const GColor8 *palette = png_decoder_data->palette;
for (uint32_t y = 0; y < fctl.height; y++) {
const uint16_t corrected_dst_y = fctl.y_offset + y + bitmap->bounds.origin.y;
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, corrected_dst_y);
int16_t delta_x = fctl.x_offset + bitmap->bounds.origin.x;
for (int32_t x = MAX(0, row_info.min_x - delta_x);
x < MIN((int32_t)fctl.width, row_info.max_x - delta_x + 1);
x++) {
const uint32_t corrected_dst_x = x + delta_x;
const uint8_t palette_index = raw_image_get_value_for_bitdepth(upng_buffer, x, y,
row_stride_bytes, bpp);
const GColor8 src = palette[palette_index];
GColor8 *const dst = (GColor8 *)(row_info.data + corrected_dst_x);
if (fctl.blend_op == APNG_BLEND_OP_OVER) {
prv_gbitmap_sequence_blend_over(src, dst);
} else {
*dst = src;
}
}
}
} else if (png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8) {
const int32_t transparent_gray = gbitmap_png_get_transparent_gray_value(upng);
for (uint32_t y = 0; y < fctl.height; y++) {
const uint16_t corrected_y = fctl.y_offset + y + bitmap->bounds.origin.y;
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, corrected_y);
// delta_x is the first bit of data in this frame relative to the bitmap's coordinate system
const int16_t delta_x = fctl.x_offset + bitmap->bounds.origin.x;
// for each pixel in this frame, clipping to the bitmap geometry
for (int32_t x = MAX(0, row_info.min_x - delta_x);
x < MIN((int32_t)fctl.width, row_info.max_x - delta_x + 1);
x++) {
const uint32_t corrected_dst_x = x + delta_x;
uint8_t channel = raw_image_get_value_for_bitdepth(upng_buffer, x, y,
row_stride_bytes, bpp);
if (transparent_gray >= 0 && channel == transparent_gray) {
// Grayscale only has fully transparent, so only modify pixels
// during OP_SOURCE to make the area transparent
if (fctl.blend_op == APNG_BLEND_OP_SOURCE) {
prv_set_pixel_in_row(row_info.data, bitmap_format, corrected_dst_x, GColorClear);
}
} else {
channel = (channel * 255) / ~(~0 << bpp); // Convert to 8-bit value
const GColor8 color = GColorFromRGB(channel, channel, channel);
prv_set_pixel_in_row(row_info.data, bitmap_format, corrected_dst_x, color);
}
}
}
}
// Successfully updated gbitmap from sequence
retval = true;
cleanup:
if (!retval) {
APP_LOG(APP_LOG_LEVEL_ERROR, APNG_UPDATE_ERROR);
applib_free(buffer);
}
return retval;
}
// total elapsed from start of animation
bool gbitmap_sequence_update_bitmap_by_elapsed(GBitmapSequence *bitmap_sequence,
GBitmap *bitmap, uint32_t elapsed_ms) {
if (!bitmap_sequence) {
return false;
}
// Disabled if play count is 0 and not the very first frame
if (bitmap_sequence->play_count == 0 && bitmap_sequence->current_frame != 0) {
return false;
}
// If animation has started and specified time is in the past
if (bitmap_sequence->current_frame_delay_ms != 0 && elapsed_ms <= bitmap_sequence->elapsed_ms) {
APP_LOG(APP_LOG_LEVEL_WARNING, APNG_ELAPSED_WARNING);
return false;
}
bool retval = false;
bool frame_updated = true;
while (frame_updated && ((elapsed_ms > bitmap_sequence->elapsed_ms) ||
(bitmap_sequence->current_frame_delay_ms == 0))) {
frame_updated = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
// If frame is updated at least once, return true
if (frame_updated) {
retval = true;
}
}
return retval;
}
// Helper functions
int32_t gbitmap_sequence_get_current_frame_idx(GBitmapSequence *bitmap_sequence) {
if (bitmap_sequence) {
return bitmap_sequence->current_frame;
}
return -1;
}
uint32_t gbitmap_sequence_get_current_frame_delay_ms(GBitmapSequence *bitmap_sequence) {
if (bitmap_sequence) {
return bitmap_sequence->current_frame_delay_ms;
}
return 0;
}
uint32_t gbitmap_sequence_get_total_num_frames(GBitmapSequence *bitmap_sequence) {
if (bitmap_sequence) {
return bitmap_sequence->total_frames;
}
return 0;
}
uint32_t gbitmap_sequence_get_play_count(GBitmapSequence *bitmap_sequence) {
if (bitmap_sequence) {
return bitmap_sequence->play_count;
}
return 0;
}
void gbitmap_sequence_set_play_count(GBitmapSequence *bitmap_sequence, uint32_t play_count) {
// Loop count is not allowed to be set to 0
if (bitmap_sequence && play_count) {
bitmap_sequence->play_count = play_count;
}
}
GSize gbitmap_sequence_get_bitmap_size(GBitmapSequence *bitmap_sequence) {
GSize size = (GSize){0, 0};
if (bitmap_sequence) {
size = bitmap_sequence->bitmap_size;
}
return size;
}
uint32_t gbitmap_sequence_get_total_duration(GBitmapSequence *bitmap_sequence) {
if (bitmap_sequence) {
return bitmap_sequence->total_duration_ms;
}
return 0;
}

View file

@ -0,0 +1,152 @@
/*
* 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.
*/
#pragma once
#include "upng.h"
#include "gtypes.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
typedef struct GBitmapSequencePNGDecoderData {
upng_t *upng;
size_t read_cursor; // relative to file start, advanced to the control chunk of the next frame
GColor8 *palette; // required for palettized images (rgba)
uint8_t palette_entries;
apng_dispose_ops last_dispose_op;
uint32_t previous_xoffset;
uint32_t previous_yoffset;
uint32_t previous_width;
uint32_t previous_height;
} GBitmapSequencePNGDecoderData;
typedef struct {
uint32_t resource_id;
union {
uint32_t flags;
struct {
bool header_loaded : 1;
bool data_is_loaded_from_flash : 1;
};
};
GSize bitmap_size; // Width & Height
uint32_t play_count; // Total number of times to play the sequence
uint32_t play_index; // Current number of times sequence was played
uint32_t total_duration_ms; // Duration of the animation in ms
uint32_t total_frames; // Total number of frames for the sequence
uint32_t current_frame; // Current frame in the sequence
uint32_t current_frame_delay_ms; // Amount of time to display the current frame
uint32_t elapsed_ms; // Total elapsed time for the sequence
// Stores internal decoder data
union {
GBitmapSequencePNGDecoderData png_decoder_data;
// potential decoder data for future formats
};
} GBitmapSequence;
//! Creates a GBitmapSequence from the specified resource (APNG/PNG files)
//! @param resource_id Resource to load and create GBitmapSequence from.
//! @return GBitmapSequence pointer if the resource was loaded, NULL otherwise
GBitmapSequence *gbitmap_sequence_create_with_resource(uint32_t resource_id);
//! @internal
GBitmapSequence *gbitmap_sequence_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id);
//! Deletes the GBitmapSequence structure and frees any allocated memory/decoder_data
//! @param bitmap_sequence Pointer to the bitmap sequence to free (delete)
void gbitmap_sequence_destroy(GBitmapSequence *bitmap_sequence);
//! Restarts the GBitmapSequence to the first frame \ref gbitmap_sequence_update_bitmap_next_frame
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return True if sequence was restarted, false otherwise
bool gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence);
//! Updates the contents of the bitmap sequence to the next frame
//! and optionally returns the delay in milliseconds until the next frame.
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @param bitmap Pointer to the initialized GBitmap in which to render the bitmap sequence
//! @param[out] delay_ms If not NULL, returns the delay in milliseconds until the next frame.
//! @return True if frame was rendered. False if all frames (and loops) have been rendered
//! for the sequence. Will also return false if frame could not be rendered
//! (includes out of memory errors).
//! @note GBitmap must be large enough to accommodate the bitmap_sequence image
//! \ref gbitmap_sequence_get_bitmap_size
bool gbitmap_sequence_update_bitmap_next_frame(GBitmapSequence *bitmap_sequence,
GBitmap *bitmap, uint32_t *delay_ms);
//! Updates the contents of the bitmap sequence to the frame at elapsed in the sequence.
//! For looping animations this accounts for the loop, for example an animation of 1 second that
//! is configured to loop 2 times updated to 1500 ms elapsed time will display the sequence
//! frame at 500 ms. Elapsed time is the time from the start of the animation, and will
//! be ignored if it is for a time earlier than the last rendered frame.
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @param bitmap Pointer to the initialized GBitmap in which to render the bitmap sequence
//! @param elapsed_ms Elapsed time in milliseconds in the sequence relative to start
//! @return True if a frame was rendered. False if all frames (and loops) have already
//! been rendered for the sequence. Will also return false if frame could not be rendered
//! (includes out of memory errors).
//! @note GBitmap must be large enough to accommodate the bitmap_sequence image
//! \ref gbitmap_sequence_get_bitmap_size
//! @note This function is disabled for play_count 0
bool gbitmap_sequence_update_bitmap_by_elapsed(GBitmapSequence *bitmap_sequence,
GBitmap *bitmap, uint32_t elapsed_ms);
//! This function gets the current frame number for the bitmap sequence
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return index of current frame in the current loop of the bitmap sequence
int32_t gbitmap_sequence_get_current_frame_idx(GBitmapSequence *bitmap_sequence);
//! This function gets the current frame's delay in milliseconds
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return delay for current frame to be shown in milliseconds
uint32_t gbitmap_sequence_get_current_frame_delay_ms(GBitmapSequence *bitmap_sequence);
//! This function sets the total number of frames for the bitmap sequence
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return number of frames contained in a single loop of the bitmap sequence
uint32_t gbitmap_sequence_get_total_num_frames(GBitmapSequence *bitmap_sequence);
//! This function gets the play count (number of times to repeat) the bitmap sequence
//! @note This value is initialized by the bitmap sequence data, and is modified by
//! \ref gbitmap_sequence_set_play_count
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return Play count of bitmap sequence, PLAY_COUNT_INFINITE for infinite looping
uint32_t gbitmap_sequence_get_play_count(GBitmapSequence *bitmap_sequence);
//! This function sets the play count (number of times to repeat) the bitmap sequence
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @param play_count Number of times to repeat the bitmap sequence
//! with 0 disabling update_by_elapsed and update_next_frame, and
//! PLAY_COUNT_INFINITE for infinite looping of the animation
void gbitmap_sequence_set_play_count(GBitmapSequence *bitmap_sequence, uint32_t play_count);
//! This function gets the minimum required size (dimensions) necessary
//! to render the bitmap sequence to a GBitmap
//! using the /ref gbitmap_sequence_update_bitmap_next_frame
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return Dimensions required to render the bitmap sequence to a GBitmap
GSize gbitmap_sequence_get_bitmap_size(GBitmapSequence *bitmap_sequence);
//! @internal
//! This function gets the total duration in milliseconds of the \ref GBitmapSequence. This does
//! not include the play count, it only refers to the duration of playing one sequence.
//! @param bitmap_sequence Pointer to loaded bitmap sequence
//! @return The total duration in milliseconds of the \ref GBitmapSequence
uint32_t gbitmap_sequence_get_total_duration(GBitmapSequence *bitmap_sequence);

View file

@ -0,0 +1,54 @@
/*
* 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 "gtypes.h"
//! This is used for performaing backward-compatibility conversions with 1-bit GColors.
GColor8 get_native_color(GColor2 color) {
switch (color) {
case GColor2Black:
return GColorBlack;
case GColor2White:
return GColorWhite;
default:
return GColorClear; // GColorClear defined as ~0, so it is everything else we may receive
}
}
GColor2 get_closest_gcolor2(GColor8 color) {
if (color.a == 0) {
return GColor2Clear;
}
switch (color.argb) {
case GColorBlackARGB8:
return GColor2Black;
case GColorWhiteARGB8:
return GColor2White;
case GColorClearARGB8:
return GColor2Clear;
default:
return GColor2White; // TODO: This should pick the closes color rather than just white.
}
}
bool gcolor_equal__deprecated(GColor8 x, GColor8 y) {
return (x.argb == y.argb);
}
bool gcolor_equal(GColor8 x, GColor8 y) {
return ((x.argb == y.argb) || ((x.a == 0) && (y.a == 0)));
}

View file

@ -0,0 +1,331 @@
/*
* 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.
*/
#pragma once
// @generated
// THIS FILE HAS BEEN GENERATED, PLEASE DON'T MODIFY ITS CONTENT MANUALLY
// USE <TINTIN_ROOT>/tools/snowy_colors.py TO MAKE CHANGES
//! @addtogroup Graphics
//! @{
//! @addtogroup GraphicsTypes
//! @{
//! Convert RGBA to GColor.
//! @param red Red value from 0 - 255
//! @param green Green value from 0 - 255
//! @param blue Blue value from 0 - 255
//! @param alpha Alpha value from 0 - 255
//! @return GColor created from the RGB values
#define GColorFromRGBA(red, green, blue, alpha) ((GColor8){ \
.a = (uint8_t)(alpha) >> 6, \
.r = (uint8_t)(red) >> 6, \
.g = (uint8_t)(green) >> 6, \
.b = (uint8_t)(blue) >> 6, \
})
//! Convert RGB to GColor.
//! @param red Red value from 0 - 255
//! @param green Green value from 0 - 255
//! @param blue Blue value from 0 - 255
//! @return GColor created from the RGB values
#define GColorFromRGB(red, green, blue) \
GColorFromRGBA(red, green, blue, 255)
//! Convert hex integer to GColor.
//! @param v Integer hex value (e.g. 0x64ff46)
//! @return GColor created from the hex value
#define GColorFromHEX(v) GColorFromRGB(((v) >> 16) & 0xff, ((v) >> 8) & 0xff, ((v) & 0xff))
//! @addtogroup ColorDefinitions Color Definitions
//! A list of all of the named colors available with links to the color map on the Pebble Developer website.
//! @{
// 8bit color values of all natively supported colors
// AARRGGBB
#define GColorBlackARGB8 ((uint8_t)0b11000000)
#define GColorOxfordBlueARGB8 ((uint8_t)0b11000001)
#define GColorDukeBlueARGB8 ((uint8_t)0b11000010)
#define GColorBlueARGB8 ((uint8_t)0b11000011)
#define GColorDarkGreenARGB8 ((uint8_t)0b11000100)
#define GColorMidnightGreenARGB8 ((uint8_t)0b11000101)
#define GColorCobaltBlueARGB8 ((uint8_t)0b11000110)
#define GColorBlueMoonARGB8 ((uint8_t)0b11000111)
#define GColorIslamicGreenARGB8 ((uint8_t)0b11001000)
#define GColorJaegerGreenARGB8 ((uint8_t)0b11001001)
#define GColorTiffanyBlueARGB8 ((uint8_t)0b11001010)
#define GColorVividCeruleanARGB8 ((uint8_t)0b11001011)
#define GColorGreenARGB8 ((uint8_t)0b11001100)
#define GColorMalachiteARGB8 ((uint8_t)0b11001101)
#define GColorMediumSpringGreenARGB8 ((uint8_t)0b11001110)
#define GColorCyanARGB8 ((uint8_t)0b11001111)
#define GColorBulgarianRoseARGB8 ((uint8_t)0b11010000)
#define GColorImperialPurpleARGB8 ((uint8_t)0b11010001)
#define GColorIndigoARGB8 ((uint8_t)0b11010010)
#define GColorElectricUltramarineARGB8 ((uint8_t)0b11010011)
#define GColorArmyGreenARGB8 ((uint8_t)0b11010100)
#define GColorDarkGrayARGB8 ((uint8_t)0b11010101)
#define GColorLibertyARGB8 ((uint8_t)0b11010110)
#define GColorVeryLightBlueARGB8 ((uint8_t)0b11010111)
#define GColorKellyGreenARGB8 ((uint8_t)0b11011000)
#define GColorMayGreenARGB8 ((uint8_t)0b11011001)
#define GColorCadetBlueARGB8 ((uint8_t)0b11011010)
#define GColorPictonBlueARGB8 ((uint8_t)0b11011011)
#define GColorBrightGreenARGB8 ((uint8_t)0b11011100)
#define GColorScreaminGreenARGB8 ((uint8_t)0b11011101)
#define GColorMediumAquamarineARGB8 ((uint8_t)0b11011110)
#define GColorElectricBlueARGB8 ((uint8_t)0b11011111)
#define GColorDarkCandyAppleRedARGB8 ((uint8_t)0b11100000)
#define GColorJazzberryJamARGB8 ((uint8_t)0b11100001)
#define GColorPurpleARGB8 ((uint8_t)0b11100010)
#define GColorVividVioletARGB8 ((uint8_t)0b11100011)
#define GColorWindsorTanARGB8 ((uint8_t)0b11100100)
#define GColorRoseValeARGB8 ((uint8_t)0b11100101)
#define GColorPurpureusARGB8 ((uint8_t)0b11100110)
#define GColorLavenderIndigoARGB8 ((uint8_t)0b11100111)
#define GColorLimerickARGB8 ((uint8_t)0b11101000)
#define GColorBrassARGB8 ((uint8_t)0b11101001)
#define GColorLightGrayARGB8 ((uint8_t)0b11101010)
#define GColorBabyBlueEyesARGB8 ((uint8_t)0b11101011)
#define GColorSpringBudARGB8 ((uint8_t)0b11101100)
#define GColorInchwormARGB8 ((uint8_t)0b11101101)
#define GColorMintGreenARGB8 ((uint8_t)0b11101110)
#define GColorCelesteARGB8 ((uint8_t)0b11101111)
#define GColorRedARGB8 ((uint8_t)0b11110000)
#define GColorFollyARGB8 ((uint8_t)0b11110001)
#define GColorFashionMagentaARGB8 ((uint8_t)0b11110010)
#define GColorMagentaARGB8 ((uint8_t)0b11110011)
#define GColorOrangeARGB8 ((uint8_t)0b11110100)
#define GColorSunsetOrangeARGB8 ((uint8_t)0b11110101)
#define GColorBrilliantRoseARGB8 ((uint8_t)0b11110110)
#define GColorShockingPinkARGB8 ((uint8_t)0b11110111)
#define GColorChromeYellowARGB8 ((uint8_t)0b11111000)
#define GColorRajahARGB8 ((uint8_t)0b11111001)
#define GColorMelonARGB8 ((uint8_t)0b11111010)
#define GColorRichBrilliantLavenderARGB8 ((uint8_t)0b11111011)
#define GColorYellowARGB8 ((uint8_t)0b11111100)
#define GColorIcterineARGB8 ((uint8_t)0b11111101)
#define GColorPastelYellowARGB8 ((uint8_t)0b11111110)
#define GColorWhiteARGB8 ((uint8_t)0b11111111)
// GColor values of all natively supported colors
//! <span class="gcolor_sample" style="background-color: #000000;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#000000">GColorBlack</a>
#define GColorBlack (GColor8){.argb=GColorBlackARGB8}
//! <span class="gcolor_sample" style="background-color: #000055;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#000055">GColorOxfordBlue</a>
#define GColorOxfordBlue (GColor8){.argb=GColorOxfordBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #0000AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#0000AA">GColorDukeBlue</a>
#define GColorDukeBlue (GColor8){.argb=GColorDukeBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #0000FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#0000FF">GColorBlue</a>
#define GColorBlue (GColor8){.argb=GColorBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #005500;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#005500">GColorDarkGreen</a>
#define GColorDarkGreen (GColor8){.argb=GColorDarkGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #005555;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#005555">GColorMidnightGreen</a>
#define GColorMidnightGreen (GColor8){.argb=GColorMidnightGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #0055AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#0055AA">GColorCobaltBlue</a>
#define GColorCobaltBlue (GColor8){.argb=GColorCobaltBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #0055FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#0055FF">GColorBlueMoon</a>
#define GColorBlueMoon (GColor8){.argb=GColorBlueMoonARGB8}
//! <span class="gcolor_sample" style="background-color: #00AA00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00AA00">GColorIslamicGreen</a>
#define GColorIslamicGreen (GColor8){.argb=GColorIslamicGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #00AA55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00AA55">GColorJaegerGreen</a>
#define GColorJaegerGreen (GColor8){.argb=GColorJaegerGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #00AAAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00AAAA">GColorTiffanyBlue</a>
#define GColorTiffanyBlue (GColor8){.argb=GColorTiffanyBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #00AAFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00AAFF">GColorVividCerulean</a>
#define GColorVividCerulean (GColor8){.argb=GColorVividCeruleanARGB8}
//! <span class="gcolor_sample" style="background-color: #00FF00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00FF00">GColorGreen</a>
#define GColorGreen (GColor8){.argb=GColorGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #00FF55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00FF55">GColorMalachite</a>
#define GColorMalachite (GColor8){.argb=GColorMalachiteARGB8}
//! <span class="gcolor_sample" style="background-color: #00FFAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00FFAA">GColorMediumSpringGreen</a>
#define GColorMediumSpringGreen (GColor8){.argb=GColorMediumSpringGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #00FFFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#00FFFF">GColorCyan</a>
#define GColorCyan (GColor8){.argb=GColorCyanARGB8}
//! <span class="gcolor_sample" style="background-color: #550000;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#550000">GColorBulgarianRose</a>
#define GColorBulgarianRose (GColor8){.argb=GColorBulgarianRoseARGB8}
//! <span class="gcolor_sample" style="background-color: #550055;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#550055">GColorImperialPurple</a>
#define GColorImperialPurple (GColor8){.argb=GColorImperialPurpleARGB8}
//! <span class="gcolor_sample" style="background-color: #5500AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#5500AA">GColorIndigo</a>
#define GColorIndigo (GColor8){.argb=GColorIndigoARGB8}
//! <span class="gcolor_sample" style="background-color: #5500FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#5500FF">GColorElectricUltramarine</a>
#define GColorElectricUltramarine (GColor8){.argb=GColorElectricUltramarineARGB8}
//! <span class="gcolor_sample" style="background-color: #555500;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#555500">GColorArmyGreen</a>
#define GColorArmyGreen (GColor8){.argb=GColorArmyGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #555555;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#555555">GColorDarkGray</a>
#define GColorDarkGray (GColor8){.argb=GColorDarkGrayARGB8}
//! <span class="gcolor_sample" style="background-color: #5555AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#5555AA">GColorLiberty</a>
#define GColorLiberty (GColor8){.argb=GColorLibertyARGB8}
//! <span class="gcolor_sample" style="background-color: #5555FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#5555FF">GColorVeryLightBlue</a>
#define GColorVeryLightBlue (GColor8){.argb=GColorVeryLightBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #55AA00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55AA00">GColorKellyGreen</a>
#define GColorKellyGreen (GColor8){.argb=GColorKellyGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #55AA55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55AA55">GColorMayGreen</a>
#define GColorMayGreen (GColor8){.argb=GColorMayGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #55AAAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55AAAA">GColorCadetBlue</a>
#define GColorCadetBlue (GColor8){.argb=GColorCadetBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #55AAFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55AAFF">GColorPictonBlue</a>
#define GColorPictonBlue (GColor8){.argb=GColorPictonBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #55FF00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55FF00">GColorBrightGreen</a>
#define GColorBrightGreen (GColor8){.argb=GColorBrightGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #55FF55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55FF55">GColorScreaminGreen</a>
#define GColorScreaminGreen (GColor8){.argb=GColorScreaminGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #55FFAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55FFAA">GColorMediumAquamarine</a>
#define GColorMediumAquamarine (GColor8){.argb=GColorMediumAquamarineARGB8}
//! <span class="gcolor_sample" style="background-color: #55FFFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#55FFFF">GColorElectricBlue</a>
#define GColorElectricBlue (GColor8){.argb=GColorElectricBlueARGB8}
//! <span class="gcolor_sample" style="background-color: #AA0000;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA0000">GColorDarkCandyAppleRed</a>
#define GColorDarkCandyAppleRed (GColor8){.argb=GColorDarkCandyAppleRedARGB8}
//! <span class="gcolor_sample" style="background-color: #AA0055;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA0055">GColorJazzberryJam</a>
#define GColorJazzberryJam (GColor8){.argb=GColorJazzberryJamARGB8}
//! <span class="gcolor_sample" style="background-color: #AA00AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA00AA">GColorPurple</a>
#define GColorPurple (GColor8){.argb=GColorPurpleARGB8}
//! <span class="gcolor_sample" style="background-color: #AA00FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA00FF">GColorVividViolet</a>
#define GColorVividViolet (GColor8){.argb=GColorVividVioletARGB8}
//! <span class="gcolor_sample" style="background-color: #AA5500;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA5500">GColorWindsorTan</a>
#define GColorWindsorTan (GColor8){.argb=GColorWindsorTanARGB8}
//! <span class="gcolor_sample" style="background-color: #AA5555;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA5555">GColorRoseVale</a>
#define GColorRoseVale (GColor8){.argb=GColorRoseValeARGB8}
//! <span class="gcolor_sample" style="background-color: #AA55AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA55AA">GColorPurpureus</a>
#define GColorPurpureus (GColor8){.argb=GColorPurpureusARGB8}
//! <span class="gcolor_sample" style="background-color: #AA55FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AA55FF">GColorLavenderIndigo</a>
#define GColorLavenderIndigo (GColor8){.argb=GColorLavenderIndigoARGB8}
//! <span class="gcolor_sample" style="background-color: #AAAA00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAAA00">GColorLimerick</a>
#define GColorLimerick (GColor8){.argb=GColorLimerickARGB8}
//! <span class="gcolor_sample" style="background-color: #AAAA55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAAA55">GColorBrass</a>
#define GColorBrass (GColor8){.argb=GColorBrassARGB8}
//! <span class="gcolor_sample" style="background-color: #AAAAAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAAAAA">GColorLightGray</a>
#define GColorLightGray (GColor8){.argb=GColorLightGrayARGB8}
//! <span class="gcolor_sample" style="background-color: #AAAAFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAAAFF">GColorBabyBlueEyes</a>
#define GColorBabyBlueEyes (GColor8){.argb=GColorBabyBlueEyesARGB8}
//! <span class="gcolor_sample" style="background-color: #AAFF00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAFF00">GColorSpringBud</a>
#define GColorSpringBud (GColor8){.argb=GColorSpringBudARGB8}
//! <span class="gcolor_sample" style="background-color: #AAFF55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAFF55">GColorInchworm</a>
#define GColorInchworm (GColor8){.argb=GColorInchwormARGB8}
//! <span class="gcolor_sample" style="background-color: #AAFFAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAFFAA">GColorMintGreen</a>
#define GColorMintGreen (GColor8){.argb=GColorMintGreenARGB8}
//! <span class="gcolor_sample" style="background-color: #AAFFFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#AAFFFF">GColorCeleste</a>
#define GColorCeleste (GColor8){.argb=GColorCelesteARGB8}
//! <span class="gcolor_sample" style="background-color: #FF0000;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF0000">GColorRed</a>
#define GColorRed (GColor8){.argb=GColorRedARGB8}
//! <span class="gcolor_sample" style="background-color: #FF0055;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF0055">GColorFolly</a>
#define GColorFolly (GColor8){.argb=GColorFollyARGB8}
//! <span class="gcolor_sample" style="background-color: #FF00AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF00AA">GColorFashionMagenta</a>
#define GColorFashionMagenta (GColor8){.argb=GColorFashionMagentaARGB8}
//! <span class="gcolor_sample" style="background-color: #FF00FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF00FF">GColorMagenta</a>
#define GColorMagenta (GColor8){.argb=GColorMagentaARGB8}
//! <span class="gcolor_sample" style="background-color: #FF5500;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF5500">GColorOrange</a>
#define GColorOrange (GColor8){.argb=GColorOrangeARGB8}
//! <span class="gcolor_sample" style="background-color: #FF5555;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF5555">GColorSunsetOrange</a>
#define GColorSunsetOrange (GColor8){.argb=GColorSunsetOrangeARGB8}
//! <span class="gcolor_sample" style="background-color: #FF55AA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF55AA">GColorBrilliantRose</a>
#define GColorBrilliantRose (GColor8){.argb=GColorBrilliantRoseARGB8}
//! <span class="gcolor_sample" style="background-color: #FF55FF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FF55FF">GColorShockingPink</a>
#define GColorShockingPink (GColor8){.argb=GColorShockingPinkARGB8}
//! <span class="gcolor_sample" style="background-color: #FFAA00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFAA00">GColorChromeYellow</a>
#define GColorChromeYellow (GColor8){.argb=GColorChromeYellowARGB8}
//! <span class="gcolor_sample" style="background-color: #FFAA55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFAA55">GColorRajah</a>
#define GColorRajah (GColor8){.argb=GColorRajahARGB8}
//! <span class="gcolor_sample" style="background-color: #FFAAAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFAAAA">GColorMelon</a>
#define GColorMelon (GColor8){.argb=GColorMelonARGB8}
//! <span class="gcolor_sample" style="background-color: #FFAAFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFAAFF">GColorRichBrilliantLavender</a>
#define GColorRichBrilliantLavender (GColor8){.argb=GColorRichBrilliantLavenderARGB8}
//! <span class="gcolor_sample" style="background-color: #FFFF00;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFFF00">GColorYellow</a>
#define GColorYellow (GColor8){.argb=GColorYellowARGB8}
//! <span class="gcolor_sample" style="background-color: #FFFF55;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFFF55">GColorIcterine</a>
#define GColorIcterine (GColor8){.argb=GColorIcterineARGB8}
//! <span class="gcolor_sample" style="background-color: #FFFFAA;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFFFAA">GColorPastelYellow</a>
#define GColorPastelYellow (GColor8){.argb=GColorPastelYellowARGB8}
//! <span class="gcolor_sample" style="background-color: #FFFFFF;"></span> <a href="https://developer.getpebble.com/tools/color-picker/#FFFFFF">GColorWhite</a>
#define GColorWhite (GColor8){.argb=GColorWhiteARGB8}
// Additional 8bit color values
#define GColorClearARGB8 ((uint8_t)0b00000000)
// Additional GColor values
#define GColorClear ((GColor8){.argb=GColorClearARGB8})
//! @} // group ColorDefinitions
//! @} // group GraphicsTypes
//! @} // group Graphics

View file

@ -0,0 +1,243 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
#include "text_layout_private.h"
#include "text_resources.h"
#define GDRAWMASK_BITS_PER_PIXEL PBL_IF_COLOR_ELSE(2, 1)
#define GDRAWMASK_PIXELS_PER_BYTE (8 / GDRAWMASK_BITS_PER_PIXEL)
//! @internal
typedef struct FrameBuffer FrameBuffer;
//! @internal
typedef struct GContext {
GBitmap dest_bitmap;
//! Which framebuffer dest_bitmap points into. This may be null if the
//! bitmap doesn't point into a framebuffer.
FrameBuffer* parent_framebuffer;
//! Number of rows between the top of the dest_bitmap and the top of it's
//! parent framebuffer. This value is invalid if parent_framebuffer is null.
uint8_t parent_framebuffer_vertical_offset;
// Keep state here for drawing commands:
GDrawState draw_state;
TextDrawState text_draw_state;
FontCache font_cache;
//! When the frame buffer accessed directly all graphics functions using this
//! context are locked
bool lock;
} GContext;
//! @internal
typedef enum {
GContextInitializationMode_App,
GContextInitializationMode_System,
} GContextInitializationMode;
//! @internal
typedef enum {
//! Pixels within the range are considered to be fully opaque
GDrawMaskRowInfoType_Opaque,
//! The opacity of the pixels within the range varies and needs individual checks
GDrawMaskRowInfoType_SemiTransparent,
} GDrawMaskRowInfoType;
//! @internal
//! Describes mask values for a given scan line.
//! The sole purpose of this data structure is performance optimization so that callers don't need
//! to test every single pixel on a GDrawMask's pixel_mask_data.
typedef struct {
//! Describes how to treat the range between .min_x and .max_x
GDrawMaskRowInfoType type;
//! Left-most pixel, 3.0 means that that pixel 3 is fully visible, 3.5 means it's half visible
Fixed_S16_3 min_x;
//! Right-most pixel, 10.7 means that pixel 10 is fully opaque
Fixed_S16_3 max_x;
} GDrawMaskRowInfo;
//! @internal
//! Describes how draw operations in GDrawRawImplementation should treat the final opacity
//! conceptually. Each pixel's alpha value should be multiplied with the corresponding
//! .pixel_mask_data of this struct.
typedef struct GDrawMask {
//! Describes the mask values for each of the scan lines
GDrawMaskRowInfo *mask_row_infos;
//! Pixel mask that follows the structure and size of the actual framebuffer
void *pixel_mask_data;
//! A contiguous block of data that contains .row_infos and .pixel_mask_data
uint8_t data[];
} GDrawMask;
//! @addtogroup Graphics
//! @{
//! @addtogroup GraphicsContext Graphics Context
//! \brief The "canvas" into which an application draws
//!
//! The Pebble OS graphics engine, inspired by several notable graphics systems, including
//! Apples Quartz 2D and its predecessor QuickDraw, provides your app with a canvas into
//! which to draw, namely, the graphics context. A graphics context is the target into which
//! graphics functions can paint, using Pebble drawing routines (see \ref Drawing,
//! \ref PathDrawing and \ref TextDrawing).
//!
//! A graphics context holds a reference to the bitmap into which to paint. It also holds the
//! current drawing state, like the current fill color, stroke color, clipping box, drawing box,
//! compositing mode, and so on. The GContext struct is the type representing the graphics context.
//!
//! For drawing in your Pebble watchface or watchapp, you won't need to create a GContext
//! yourself. In most cases, it is provided by Pebble OS as an argument passed into a render
//! callback (the .update_proc of a Layer).
//!
//! Your app cant call drawing functions at any given point in time: Pebble OS will request your
//! app to render. Typically, your app will be calling out to graphics functions in
//! the .update_proc callback of a Layer.
//! @see \ref Layer
//! @see \ref Drawing
//! @see \ref PathDrawing
//! @see \ref TextDrawing
//! @{
//! @internal
void graphics_context_init(GContext *ctx, FrameBuffer *framebuffer,
GContextInitializationMode init_mode);
//! @internal
void graphics_context_set_default_drawing_state(GContext *ctx,
GContextInitializationMode init_mode);
//! @internal
//! Gets the current drawing state (fill/stroke/text colors, compositing mode, ...)
GDrawState graphics_context_get_drawing_state(GContext* ctx);
//! @internal
//! Sets the current drawing state (fill/stroke/text colors, compositing mode, ...)
void graphics_context_set_drawing_state(GContext* ctx, GDrawState draw_state);
//! @internal
//! Move the drawing box origin by the translation offset specified
void graphics_context_move_draw_box(GContext* ctx, GPoint offset);
//! Sets the current stroke color of the graphics context.
//! @param ctx The graphics context onto which to set the stroke color
//! @param color The new stroke color
void graphics_context_set_stroke_color(GContext* ctx, GColor color);
void graphics_context_set_stroke_color_2bit(GContext* ctx, GColor2 color);
//! Sets the current fill color of the graphics context.
//! @param ctx The graphics context onto which to set the fill color
//! @param color The new fill color
void graphics_context_set_fill_color(GContext* ctx, GColor color);
void graphics_context_set_fill_color_2bit(GContext* ctx, GColor2 color);
//! Sets the current text color of the graphics context.
//! @param ctx The graphics context onto which to set the text color
//! @param color The new text color
void graphics_context_set_text_color(GContext* ctx, GColor color);
void graphics_context_set_text_color_2bit(GContext* ctx, GColor2 color);
//! Sets the tint color of the graphics context. This is used when drawing under
//! the GCompOpOr compositing mode.
//! @param ctx The graphics context onto which to set the tint color
//! @param color The new tint color
void graphics_context_set_tint_color(GContext *ctx, GColor color);
//! Sets the current bitmap compositing mode of the graphics context.
//! @param ctx The graphics context onto which to set the compositing mode
//! @param mode The new compositing mode
//! @see \ref GCompOp
//! @see \ref bitmap_layer_set_compositing_mode()
//! @note At the moment, this only affects the bitmaps drawing operations
//! -- \ref graphics_draw_bitmap_in_rect(), \ref graphics_draw_rotated_bitmap, and
//! anything that uses those APIs --, but it currently does not affect the filling or stroking
//! operations.
void graphics_context_set_compositing_mode(GContext* ctx, GCompOp mode);
//! Sets whether antialiasing is applied to stroke drawing
//! @param ctx The graphics context onto which to set the antialiasing
//! @param enable True = antialiasing enabled, False = antialiasing disabled
//! @note Default value is true.
void graphics_context_set_antialiased(GContext* ctx, bool enable);
//! @internal
//! Gets whether antialiasing is applied to stroke drawing
//! @param ctx The graphics context for which to get the current state of antialiasing
//! @return True if antialiasing is enabled, false otherwise
bool graphics_context_get_antialiased(GContext *ctx);
//! Sets the width of the stroke for drawing routines
//! @param ctx The graphics context onto which to set the stroke width
//! @param stroke_width Width in pixels of the stroke.
//! @note If stroke width of zero is passed, it will be ignored and will not change the value
//! stored in GContext. Currently, only odd stroke_width values are supported. If an even value
//! is passed in, the value will be stored as is, but the drawing routines will round down to the
//! previous integral value when drawing. Default value is 1.
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width);
//! Instantiates and initializes a mask.
//! @param ctx The graphics context to use to initialize the new mask
//! @param transparent Whether the initial mask pixel values should all be transparent or opaque
//! @return The new clipping mask, or NULL on failure
GDrawMask *graphics_context_mask_create(const GContext *ctx, bool transparent);
//! Attaches a mask to the provided GContext for recording. Subsequent drawing operations will
//! change the mask values. The luminance of the drawing operations corresponds with the resulting
//! opacity in the mask, so the brighter a drawn pixel is, the more opaque its corresponding mask
//! value will be.
//! @param ctx The GContext to attach the mask to for recording
//! @param mask The mask to use for recording
//! @return True if the mask was successfully attached to the GContext for recording, false
//! otherwise
bool graphics_context_mask_record(GContext *ctx, GDrawMask *mask);
//! Attaches a mask to the provided GContext and activates it for subsequent drawing operations.
//! Upon activation, subsequent drawing operations will be multiplied with the given mask.
//! @param ctx The GContext to attach the mask to for use
//! @param mask The mask to use
//! @return True if the mask was successfully attached to the GContext for use, false otherwise
bool graphics_context_mask_use(GContext *ctx, GDrawMask *mask);
//! Destroys a previously created mask.
//! @param ctx The GContext the mask was used with
//! @param mask The mask to destroy
void graphics_context_mask_destroy(GContext *ctx, GDrawMask *mask);
//! @internal
//! Gets the size of the backing framebuffer for the graphics context or GSize(DISP_COLS, DISP_ROWS)
//! if there is no backing framebuffer.
GSize graphics_context_get_framebuffer_size(GContext *ctx);
//! @internal
//! Retreives the destination bitmap for the graphics context.
//! @param ctx The graphics context to retreive the bitmap for.
GBitmap* graphics_context_get_bitmap(GContext* ctx);
//! @internal
//! Updates the parent framebuffers dirty state based on a change to the
//! graphic context's bitmap.
void graphics_context_mark_dirty_rect(GContext* ctx, GRect rect);
//! @} // end addtogroup GraphicsContext
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,265 @@
/*
* 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 "applib/applib_resource_private.h"
#include "gdraw_command.h"
#include "gdraw_command_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/gpath.h"
#include "system/passert.h"
#include "syscall/syscall.h"
#include "util/net.h"
bool gdraw_command_resource_is_valid(ResAppNum app_num, uint32_t resource_id,
uint32_t expected_signature, uint32_t *data_size) {
// Load file signature, and check that it matches the expected_signature
uint32_t data_signature;
if (!(sys_resource_load_range(app_num, resource_id, 0, (uint8_t*)&data_signature,
sizeof(data_signature)) == sizeof(data_signature) &&
(ntohl(data_signature) == expected_signature))) {
return NULL;
}
// Data is the second entry after the resource signature
if (data_size) {
uint32_t output_data_size;
_Static_assert(PDCI_SIZE_OFFSET == PDCS_SIZE_OFFSET,
"code re-use between PDCI/PDCS requires same file format header");
if (sys_resource_load_range(app_num, resource_id, sizeof(expected_signature),
(uint8_t*)&output_data_size, sizeof(output_data_size)) != sizeof(output_data_size)) {
return NULL;
}
*data_size = output_data_size;
}
return true;
}
bool gdraw_command_validate(GDrawCommand *command, size_t size) {
if ((size < gdraw_command_get_data_size(command))) {
return false;
}
return (((command->type == GDrawCommandTypeCircle) && (command->num_points == 1)) ||
((command->type == GDrawCommandTypePath) && (command->num_points > 1)) ||
((command->type == GDrawCommandTypePrecisePath) && (command->num_points > 1)));
}
static void prv_draw_path(GContext *ctx, GDrawCommand *command) {
if (command->num_points <= 1) {
return;
}
GPath path = {
.num_points = command->num_points,
.points = command->points
};
// draw all values of alpha, except fully transparent
if ((command->fill_color.a != 0)) {
graphics_context_set_fill_color(ctx, command->fill_color);
gpath_draw_filled(ctx, &path);
}
if ((command->stroke_color.a != 0) && (command->stroke_width > 0)) {
graphics_context_set_stroke_color(ctx, command->stroke_color);
graphics_context_set_stroke_width(ctx, command->stroke_width);
gpath_draw_stroke(ctx, &path, command->path_open);
}
}
static void prv_draw_circle(GContext *ctx, GDrawCommand *command) {
// draw all values of alpha, except fully transparent
if ((command->fill_color.a != 0) && (command->radius > 0)) {
graphics_context_set_fill_color(ctx, command->fill_color);
graphics_fill_circle(ctx, command->points[0], command->radius);
}
if ((command->stroke_color.a != 0) && (command->stroke_width > 0)) {
graphics_context_set_stroke_color(ctx, command->stroke_color);
graphics_context_set_stroke_width(ctx, command->stroke_width);
graphics_draw_circle(ctx, command->points[0], command->radius);
}
}
static void prv_draw_precise_path(GContext *ctx, GDrawCommand *command) {
if (command->num_points <= 1) {
return;
}
// draw all values of alpha, except fully transparent
if ((command->fill_color.a != 0)) {
graphics_context_set_fill_color(ctx, command->fill_color);
gpath_fill_precise_internal(ctx, command->precise_points, command->num_precise_points);
}
if ((command->stroke_color.a != 0) && (command->stroke_width > 0)) {
graphics_context_set_stroke_color(ctx, command->stroke_color);
graphics_context_set_stroke_width(ctx, command->stroke_width);
gpath_draw_outline_precise_internal(ctx, command->precise_points,
command->num_precise_points, command->path_open);
}
}
void gdraw_command_draw(GContext *ctx, GDrawCommand *command) {
if (!command || command->hidden) {
return;
}
switch (command->type) {
case GDrawCommandTypePath:
prv_draw_path(ctx, command);
break;
case GDrawCommandTypePrecisePath:
prv_draw_precise_path(ctx, command);
break;
case GDrawCommandTypeCircle:
prv_draw_circle(ctx, command);
break;
default:
WTF;
}
}
size_t gdraw_command_get_data_size(GDrawCommand *command) {
if (!command) {
return 0;
}
return (sizeof(GDrawCommand) + (command->num_points * sizeof(GPoint)));
}
GDrawCommandType gdraw_command_get_type(GDrawCommand *command) {
if (!command) {
return GDrawCommandTypeInvalid;
}
return command->type;
}
void gdraw_command_set_fill_color(GDrawCommand *command, GColor fill_color) {
if (!command) {
return;
}
command->fill_color = fill_color;
}
GColor gdraw_command_get_fill_color(GDrawCommand *command) {
if (!command) {
return (GColor) {0};
} else {
return command->fill_color;
}
}
void gdraw_command_set_stroke_color(GDrawCommand *command, GColor stroke_color) {
if (!command) {
return;
} else {
command->stroke_color = stroke_color;
}
}
GColor gdraw_command_get_stroke_color(GDrawCommand *command) {
if (!command) {
return (GColor) {0};
} else {
return command->stroke_color;
}
}
void gdraw_command_set_stroke_width(GDrawCommand *command, uint8_t stroke_width) {
if (!command) {
return;
}
command->stroke_width = stroke_width;
}
uint8_t gdraw_command_get_stroke_width(GDrawCommand *command) {
if (!command) {
return 0;
}
return command->stroke_width;
}
uint16_t gdraw_command_get_num_points(GDrawCommand *command) {
if (!command) {
return 0;
}
return command->num_points;
}
void gdraw_command_set_point(GDrawCommand *command, uint16_t point_idx, GPoint point) {
if (!command || (point_idx >= command->num_points)) {
return;
}
command->points[point_idx] = point;
}
GPoint gdraw_command_get_point(GDrawCommand *command, uint16_t point_idx) {
if (!command || (point_idx >= command->num_points)) {
return GPointZero;
} else {
return command->points[point_idx];
}
}
void gdraw_command_set_radius(GDrawCommand *command, uint16_t radius) {
if (!command || (command->type != GDrawCommandTypeCircle)) {
return;
}
command->radius = radius;
}
uint16_t gdraw_command_get_radius(GDrawCommand *command) {
if (!command || (command->type != GDrawCommandTypeCircle)) {
return 0;
}
return command->radius;
}
void gdraw_command_set_path_open(GDrawCommand *command, bool path_open) {
if (!command ||
((command->type != GDrawCommandTypePath) &&
(command->type != GDrawCommandTypePrecisePath))) {
return;
}
command->path_open = path_open;
}
bool gdraw_command_get_path_open(GDrawCommand *command) {
if (!command ||
((command->type != GDrawCommandTypePath) &&
(command->type != GDrawCommandTypePrecisePath))) {
return false;
}
return command->path_open;
}
void gdraw_command_set_hidden(GDrawCommand *command, bool hidden) {
if (!command) {
return;
}
command->hidden = hidden;
}
bool gdraw_command_get_hidden(GDrawCommand *command) {
if (!command) {
return false;
}
return command->hidden;
}
size_t gdraw_command_copy_points(GDrawCommand *command, GPoint *points, const size_t max_bytes) {
const size_t actual_size = gdraw_command_get_num_points(command) * sizeof(GPoint);
const size_t size = MIN(max_bytes, actual_size);
memcpy(points, command->points, size);
return size;
}

View file

@ -0,0 +1,196 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include <stdint.h>
#include <stdbool.h>
//! @file graphics/gdraw_command.h
//! Defines the basic functions available to manipulate Pebble Draw Commands
//! @addtogroup Graphics
//! @{
//! @addtogroup DrawCommand Draw Commands
//! \brief Pebble Draw Commands are a way to encode arbitrary path draw and fill calls in binary
//! format, so that vector-like graphics can be represented on the watch.
//!
//! These draw commands can
//! be loaded from resources, manipulated in place and drawn to the current graphics context. Each
//! \ref GDrawCommand can be an arbitrary path or a circle with optional fill or stroke. The stroke
//! width and color of the stroke and fill are also encoded within the \ref GDrawCommand. Paths can
//! can be drawn open or closed.
//!
//! All aspects of a draw command can be modified, except for the number of points in a path (a
//! circle only has one point, the center).
//!
//! Draw commands are grouped into a \ref GDrawCommandList, which can be drawn all at once.
//! Each individual \ref GDrawCommand can be accessed from a \ref GDrawCommandList for modification.
//!
//! A \ref GDrawCommandList forms the basis for \ref GDrawCommandImage and \ref GDrawCommandFrame
//! objects. A \ref GDrawCommandImage represents a static image and can be represented by the PDC
//! file format and can be loaded as a resource.
//!
//! Once you have a \ref GDrawCommandImage loaded in memory you can draw it on the screen in a
//! \ref LayerUpdateProc with the \ref gdraw_command_image_draw().
//!
//! A \ref GDrawCommandFrame represents a single frame of an animated sequence, with multiple frames
//! making up a single \ref GDrawCommandSequence, which can also be stored as a PDC and loaded as a
//! resource.
//!
//! To draw a \ref GDrawCommandSequence, use the \ref gdraw_command_sequence_get_frame_by_elapsed()
//! to obtain the current \ref GDrawCommandFrame and \ref gdraw_command_frame_draw() to draw it.
//!
//! Draw commands also allow access to drawing with sub-pixel precision. The points are treated as
//! Fixed point types in the format 13.3, so that 1/8th of a pixel precision is possible. Only the
//! points in draw commands of the type GDrawCommandTypePrecisePath will be treated as higher
//! precision.
//!
//! @{
typedef enum {
GDrawCommandTypeInvalid = 0, //!< Invalid draw command type
GDrawCommandTypePath, //!< Arbitrary path draw command type
GDrawCommandTypeCircle, //!< Circle draw command type
GDrawCommandTypePrecisePath, //!< Arbitrary path drawn with sub-pixel precision (1/8th precision)
} GDrawCommandType;
struct GDrawCommand;
//! Draw commands are the basic building block of the draw command system, encoding the type of
//! command to draw, the stroke width and color, fill color, and points that define the path (or
//! center of a circle
typedef struct GDrawCommand GDrawCommand;
//! @internal
//! Use to check the file signature on a PDC resource
bool gdraw_command_resource_is_valid(ResAppNum res_app, uint32_t resource_id,
uint32_t expected_signature, uint32_t *data_size);
//! @internal
//! Use to validate data stored as a draw command
bool gdraw_command_validate(GDrawCommand *command, size_t size);
//! Draw a command
//! @param ctx The destination graphics context in which to draw
//! @param command \ref GDrawCommand to draw
void gdraw_command_draw(GContext *ctx, GDrawCommand *command);
//! @internal
//! Get the size of a command in memory
size_t gdraw_command_get_data_size(GDrawCommand *command);
//! Get the command type
//! @param command \ref GDrawCommand from which to get the type
//! @return The type of the given \ref GDrawCommand
GDrawCommandType gdraw_command_get_type(GDrawCommand *command);
//! Set the fill color of a command
//! @param command ref DrawCommand for which to set the fill color
//! @param fill_color \ref GColor to set for the fill
void gdraw_command_set_fill_color(GDrawCommand *command, GColor fill_color);
//! Get the fill color of a command
//! @param command \ref GDrawCommand from which to get the fill color
//! @return fill color of the given \ref GDrawCommand
GColor gdraw_command_get_fill_color(GDrawCommand *command);
//! Set the stroke color of a command
//! @param command \ref GDrawCommand for which to set the stroke color
//! @param stroke_color \ref GColor to set for the stroke
void gdraw_command_set_stroke_color(GDrawCommand *command, GColor stroke_color);
//! Get the stroke color of a command
//! @param command \ref GDrawCommand from which to get the stroke color
//! @return The stroke color of the given \ref GDrawCommand
GColor gdraw_command_get_stroke_color(GDrawCommand *command);
//! Set the stroke width of a command
//! @param command \ref GDrawCommand for which to set the stroke width
//! @param stroke_width stroke width to set for the command
void gdraw_command_set_stroke_width(GDrawCommand *command, uint8_t stroke_width);
//! Get the stroke width of a command
//! @param command \ref GDrawCommand from which to get the stroke width
//! @return The stroke width of the given \ref GDrawCommand
uint8_t gdraw_command_get_stroke_width(GDrawCommand *command);
//! Get the number of points in a command
uint16_t gdraw_command_get_num_points(GDrawCommand *command);
//! Set the value of the point in a command at the specified index
//! @param command \ref GDrawCommand for which to set the value of a point
//! @param point_idx Index of the point to set the value for
//! @param point new point value to set
void gdraw_command_set_point(GDrawCommand *command, uint16_t point_idx, GPoint point);
//! Get the value of a point in a command from the specified index
//! @param command \ref GDrawCommand from which to get a point
//! @param point_idx The index to get the point for
//! @return The point in the \ref GDrawCommand specified by point_idx
//! @note The index \b must be less than the number of points
GPoint gdraw_command_get_point(GDrawCommand *command, uint16_t point_idx);
//! Set the radius of a circle command
//! @note This only works for commands of type \ref GDrawCommandCircle
//! @param command \ref GDrawCommand from which to set the circle radius
//! @param radius The radius to set for the circle.
void gdraw_command_set_radius(GDrawCommand *command, uint16_t radius);
//! Get the radius of a circle command.
//! @note this only works for commands of type\ref GDrawCommandCircle.
//! @param command \ref GDrawCommand from which to get the circle radius
//! @return The radius in pixels if command is of type \ref GDrawCommandCircle
uint16_t gdraw_command_get_radius(GDrawCommand *command);
//! Set the path of a stroke command to be open
//! @note This only works for commands of type \ref GDrawCommandPath and
//! \ref GDrawCommandPrecisePath
//! @param command \ref GDrawCommand for which to set the path open status
//! @param path_open true if path should be hidden
void gdraw_command_set_path_open(GDrawCommand *command, bool path_open);
//! Return whether a stroke command path is open
//! @note This only works for commands of type \ref GDrawCommandPath and
//! \ref GDrawCommandPrecisePath
//! @param command \ref GDrawCommand from which to get the path open status
//! @return true if the path is open
bool gdraw_command_get_path_open(GDrawCommand *command);
//! Set a command as hidden. This command will not be drawn when \ref gdraw_command_draw is called
//! with this command
//! @param command \ref GDrawCommand for which to set the hidden status
//! @param hidden true if command should be hidden
void gdraw_command_set_hidden(GDrawCommand *command, bool hidden);
//! Return whether a command is hidden
//! @param command \ref GDrawCommand from which to get the hidden status
//! @return true if command is hidden
bool gdraw_command_get_hidden(GDrawCommand *command);
//! @internal
//! Copy the points from command to a given buffer
//! The buffer should be at least the number points * sizeof(GPoint)
//! @param points the points buffer GPoints will be copied into
//! @param max_bytes the points buffer size
//! Use gdraw_command_get_num_points to correctly size the buffer
//! @return the amount of bytes that were copied
size_t gdraw_command_copy_points(GDrawCommand *command, GPoint *points, const size_t max_bytes);
//! @} // end addtogroup DrawCommand
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,85 @@
/*
* 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 "gdraw_command_frame.h"
#include "gdraw_command_private.h"
#include "system/passert.h"
bool gdraw_command_frame_validate(GDrawCommandFrame *frame, size_t size) {
if (!frame || (size < sizeof(GDrawCommandFrame))) {
return false;
}
return gdraw_command_list_validate(&frame->command_list, size - (sizeof(GDrawCommandFrame) -
sizeof(GDrawCommandList)));
}
void gdraw_command_frame_draw_processed(GContext *ctx, GDrawCommandSequence *sequence,
GDrawCommandFrame *frame, GPoint offset,
GDrawCommandProcessor *processor) {
if (!ctx || !frame) {
return;
}
// Note: sequence is passed in here to enable version handling in the future (version field in
// sequence struct will be used)
// Offset graphics context drawing box origin by specified amount
graphics_context_move_draw_box(ctx, offset);
gdraw_command_list_draw_processed(ctx, &frame->command_list, processor);
// Offset graphics context drawing box back to previous origin
graphics_context_move_draw_box(ctx, GPoint(-offset.x, -offset.y));
}
void gdraw_command_frame_draw(GContext *ctx, GDrawCommandSequence *sequence,
GDrawCommandFrame *frame, GPoint offset) {
gdraw_command_frame_draw_processed(ctx, sequence, frame, offset, NULL);
}
void gdraw_command_frame_set_duration(GDrawCommandFrame *frame, uint32_t duration) {
if (!frame) {
return;
}
frame->duration = duration;
}
uint32_t gdraw_command_frame_get_duration(GDrawCommandFrame *frame) {
if (!frame) {
return 0;
}
return frame->duration;
}
size_t gdraw_command_frame_get_data_size(GDrawCommandFrame *frame) {
if (!frame) {
return 0;
}
return sizeof(GDrawCommandFrame) - sizeof(GDrawCommandList) +
gdraw_command_list_get_data_size(&frame->command_list);
}
GDrawCommandList *gdraw_command_frame_get_command_list(GDrawCommandFrame *frame) {
if (!frame) {
return NULL;
}
return &frame->command_list;
}

View file

@ -0,0 +1,79 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/graphics.h"
#include "applib/graphics/gdraw_command_list.h"
#include <stdint.h>
#include <stdbool.h>
//! @file graphics/gdraw_command_frame.h
//! Defines the functions to manipulate \ref GDrawCommandFrame objects
//! @addtogroup Graphics
//! @{
//! @addtogroup DrawCommand Draw Commands
//! @{
struct GDrawCommandFrame;
typedef struct GDrawCommandSequence GDrawCommandSequence;
//! Draw command frames contain a list of commands to draw for that frame and a duration,
//! indicating the length of time for which the frame should be drawn in an animation sequence.
//! Frames form the building blocks of a \ref GDrawCommandSequence, which consists of multiple
//! frames.
typedef struct GDrawCommandFrame GDrawCommandFrame;
//! @internal
//! Use to validate a frame read from flash or copied from serialized data
//! @param size Size of the frame structure in memory, in bytes
bool gdraw_command_frame_validate(GDrawCommandFrame *frame, size_t size);
//! Draw a frame
//! @param ctx The destination graphics context in which to draw
//! @param sequence The sequence from which the frame comes from (this is required)
//! @param frame Frame to draw
//! @param offset Offset from draw context origin to draw the frame
void gdraw_command_frame_draw(GContext *ctx, GDrawCommandSequence *sequence,
GDrawCommandFrame *frame, GPoint offset);
//! @internal
void gdraw_command_frame_draw_processed(GContext *ctx, GDrawCommandSequence *sequence,
GDrawCommandFrame *frame, GPoint offset,
GDrawCommandProcessor *processor);
//! Set the duration of the frame
//! @param frame \ref GDrawCommandFrame for which to set the duration
//! @param duration duration of the frame in milliseconds
void gdraw_command_frame_set_duration(GDrawCommandFrame *frame, uint32_t duration);
//! Get the duration of the frame
//! @param frame \ref GDrawCommandFrame from which to get the duration
//! @return duration of the frame in milliseconds
uint32_t gdraw_command_frame_get_duration(GDrawCommandFrame *frame);
//! @internal
//! Get the size, in bytes, of the frame in memory
size_t gdraw_command_frame_get_data_size(GDrawCommandFrame *frame);
//! Get the command list of the frame
//! @param frame \ref GDrawCommandFrame from which to get the command list
//! @return command list
GDrawCommandList *gdraw_command_frame_get_command_list(GDrawCommandFrame *frame);
//! @} // end addtogroup DrawCommand
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,142 @@
/*
* 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 "gdraw_command_image.h"
#include "gdraw_command_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/applib_resource_private.h"
#include "syscall/syscall.h"
GDrawCommandImage *gdraw_command_image_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return gdraw_command_image_create_with_resource_system(app_num, resource_id);
}
GDrawCommandImage *gdraw_command_image_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id) {
uint32_t data_size;
if (!gdraw_command_resource_is_valid(app_num, resource_id, PDCI_SIGNATURE, &data_size)) {
return NULL;
}
GDrawCommandImage *draw_command_image = applib_resource_mmap_or_load(app_num, resource_id,
PDCI_DATA_OFFSET, data_size,
false);
// Validate the loaded command image
if (!gdraw_command_image_validate(draw_command_image, data_size)) {
gdraw_command_image_destroy(draw_command_image);
return NULL;
}
return draw_command_image;
}
bool gdraw_command_image_copy(void *buffer, size_t buffer_length, GDrawCommandImage *src) {
size_t src_size = gdraw_command_image_get_data_size(src);
if (buffer_length < src_size) {
return false;
}
memcpy(buffer, src, src_size);
return true;
}
GDrawCommandImage *gdraw_command_image_clone(GDrawCommandImage *image) {
if (!image) {
return NULL;
}
// potentially extracting into a generic task_ptrdup(void *, size_t)
size_t size = gdraw_command_image_get_data_size(image);
GDrawCommandImage *result = applib_malloc(size);
if (result) {
memcpy(result, image, size);
}
return result;
}
void gdraw_command_image_destroy(GDrawCommandImage *image) {
applib_resource_munmap_or_free(image);
}
bool gdraw_command_image_validate(GDrawCommandImage *image, size_t size) {
if (!image ||
(size < sizeof(GDrawCommandImage)) ||
(image->version > GDRAW_COMMAND_VERSION) ||
!gdraw_command_list_validate(&image->command_list, size - (sizeof(GDrawCommandImage) -
sizeof(GDrawCommandList)))) {
return false;
}
uint8_t *end = (uint8_t *)image + size;
return (end == gdraw_command_list_iterate_private(&image->command_list, NULL, NULL));
}
void gdraw_command_image_draw(GContext *ctx, GDrawCommandImage *image, GPoint offset) {
gdraw_command_image_draw_processed(ctx, image, offset, NULL);
}
void gdraw_command_image_draw_processed(GContext *ctx, GDrawCommandImage *image, GPoint offset,
GDrawCommandProcessor *processor) {
if (!ctx || !image) {
return;
}
// Offset graphics context drawing box origin by specified amount
graphics_context_move_draw_box(ctx, offset);
gdraw_command_list_draw_processed(ctx, &image->command_list, processor);
// Offset graphics context drawing box back to previous origin
graphics_context_move_draw_box(ctx, GPoint(-offset.x, -offset.y));
}
size_t gdraw_command_image_get_data_size(GDrawCommandImage *image) {
if (!image) {
return 0;
}
return sizeof(GDrawCommandImage) - sizeof(GDrawCommandList)
+ gdraw_command_list_get_data_size(&image->command_list);
}
GSize gdraw_command_image_get_bounds_size(GDrawCommandImage *image) {
if (!image) {
return GSizeZero;
}
return image->size;
}
void gdraw_command_image_set_bounds_size(GDrawCommandImage *image, GSize size) {
if (!image) {
return;
}
image->size = size;
}
GDrawCommandList *gdraw_command_image_get_command_list(GDrawCommandImage *image) {
if (!image) {
return NULL;
}
return &image->command_list;
}

View file

@ -0,0 +1,107 @@
/*
* 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.
*/
#pragma once
#include "gdraw_command_list.h"
#include "applib/graphics/graphics.h"
#include <stdint.h>
#include <stdbool.h>
//! @file graphics/gdraw_command_image.h
//! Defines the functions to manipulate \ref GDrawCommandImage objects
//! @addtogroup Graphics
//! @{
//! @addtogroup DrawCommand Draw Commands
//! @{
struct GDrawCommandImage;
//! Draw command images contain a list of commands that can be drawn. An image can be loaded from
//! PDC file data.
typedef struct GDrawCommandImage GDrawCommandImage;
//! Creates a GDrawCommandImage from the specified resource (PDC file)
//! @param resource_id Resource containing data to load and create GDrawCommandImage from.
//! @return GDrawCommandImage pointer if the resource was loaded, NULL otherwise
GDrawCommandImage *gdraw_command_image_create_with_resource(uint32_t resource_id);
//! @internal
GDrawCommandImage *gdraw_command_image_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id);
//! @internal
//! Copies a GDrawCommandImage into a memory buffer. The buffer length must be equal to or larger
//! than the source image.
//! @param buffer A buffer that will become a copy of the source image
//! @param buffer_length Size of the buffer in bytes
//! @param image GDrawCommandImage that will be copied from
//! @return true if the image was copied over
bool gdraw_command_image_copy(void *buffer, size_t buffer_length, GDrawCommandImage *image);
//! Creates a GDrawCommandImage as a copy from a given image
//! @param image Image to copy.
//! @return cloned image or NULL if the operation failed
GDrawCommandImage *gdraw_command_image_clone(GDrawCommandImage *image);
//! Deletes the GDrawCommandImage structure and frees associated data
//! @param image Pointer to the image to free (delete)
void gdraw_command_image_destroy(GDrawCommandImage *image);
//! @internal
//! Use to validate an image read from flash or copied from serialized data
//! @param size Size of the frame structure in memory, in bytes
bool gdraw_command_image_validate(GDrawCommandImage *image, size_t size);
//! Draw an image
//! @param ctx The destination graphics context in which to draw
//! @param image Image to draw
//! @param offset Offset from draw context origin to draw the image
void gdraw_command_image_draw(GContext *ctx, GDrawCommandImage *image, GPoint offset);
//! Draw an image after being processed by the passed in proccessor
//! @param ctx The destination graphics context in which to draw
//! @param image Image to draw
//! @param offset Offset from draw context origin to draw the image
//! @param processors Contains function pointers to draw modified commands in the image
void gdraw_command_image_draw_processed(GContext *ctx, GDrawCommandImage *image, GPoint offset,
GDrawCommandProcessor *processor);
//! @internal
//! Get the size, in bytes, of the image in memory
size_t gdraw_command_image_get_data_size(GDrawCommandImage *image);
//! Get size of the bounding box surrounding all draw commands in the image. This bounding
//! box can be used to set the graphics context or layer bounds when drawing the image.
//! @param image \ref GDrawCommandImage from which to get the bounding box size
//! @return bounding box size
GSize gdraw_command_image_get_bounds_size(GDrawCommandImage *image);
//! Set size of the bounding box surrounding all draw commands in the image. This bounding
//! box can be used to set the graphics context or layer bounds when drawing the image.
//! @param image \ref GDrawCommandImage for which to set the bounding box size
//! @param size bounding box size
void gdraw_command_image_set_bounds_size(GDrawCommandImage *image, GSize size);
//! Get the command list of the image
//! @param image \ref GDrawCommandImage from which to get the command list
//! @return command list
GDrawCommandList *gdraw_command_image_get_command_list(GDrawCommandImage *image);
//! @} // end addtogroup DrawCommand
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,296 @@
/*
* 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 "gdraw_command_list.h"
#include "gdraw_command_private.h"
#include "applib/applib_malloc.auto.h"
#include "system/passert.h"
bool gdraw_command_list_copy(void *buffer, size_t buffer_length, GDrawCommandList *src) {
size_t src_size = gdraw_command_list_get_data_size(src);
if (buffer_length < src_size) {
return false;
}
memcpy(buffer, src, src_size);
return true;
}
GDrawCommandList *gdraw_command_list_clone(GDrawCommandList *list) {
if (!list) {
return NULL;
}
size_t size = gdraw_command_list_get_data_size(list);
GDrawCommandList *result = applib_malloc(size);
if (result) {
memcpy(result, list, size);
}
return result;
}
void gdraw_command_list_destroy(GDrawCommandList *list) {
if (list) {
applib_free(list);
}
}
static GDrawCommand *prv_next_command(GDrawCommand *command) {
return (GDrawCommand *) (command->points + command->num_points);
}
bool gdraw_command_list_validate(GDrawCommandList *command_list, size_t size) {
if (!command_list ||
(size < sizeof(GDrawCommandList)) ||
(command_list->num_commands == 0)) {
return false;
}
uint8_t *end = (uint8_t *)command_list + size;
GDrawCommand *command = command_list->commands;
for (uint32_t i = 0; i < command_list->num_commands; i++) {
if ((end <= (uint8_t *) command) ||
!gdraw_command_validate(command, end - (uint8_t *) command)) {
return false;
}
command = prv_next_command(command);
}
return ((uint8_t *) command <= end);
}
void *gdraw_command_list_iterate_private(GDrawCommandList *command_list,
GDrawCommandListIteratorCb handle_command,
void *callback_context) {
if (!command_list) {
return NULL;
}
GDrawCommand *command = command_list->commands;
for (uint32_t i = 0; i < command_list->num_commands; i++) {
if ((handle_command) && (!handle_command(command, i, callback_context))) {
break;
}
command = prv_next_command(command);
}
return command;
}
void gdraw_command_list_iterate(GDrawCommandList *command_list,
GDrawCommandListIteratorCb handle_command,
void *callback_context) {
gdraw_command_list_iterate_private(command_list, handle_command, callback_context);
}
GDrawCommand *gdraw_command_list_get_command(GDrawCommandList *command_list, uint16_t command_idx) {
if (!command_list || (command_idx >= command_list->num_commands)) {
return NULL;
}
GDrawCommand *command = command_list->commands;
for (uint32_t i = 0; i < command_idx; i++) {
command = prv_next_command(command);
}
return command;
}
static bool prv_draw_command(GDrawCommand *command, uint32_t idx, void *ctx) {
gdraw_command_draw(ctx, command);
return true;
}
typedef struct {
GContext *ctx;
const GDrawCommandList *list;
GDrawCommandProcessor *processor;
GDrawCommand *processed_draw_command;
} GDrawCommandDrawProcessedCBData;
static bool prv_draw_command_processed(GDrawCommand *draw_command, uint32_t idx, void *ctx) {
GDrawCommandDrawProcessedCBData *data = ctx;
size_t size = gdraw_command_get_data_size(draw_command);
memset(data->processed_draw_command, 0, size);
memcpy(data->processed_draw_command, draw_command, size);
if (data->processor->command) {
data->processor->command(data->processor, data->processed_draw_command, size, data->list,
draw_command);
}
gdraw_command_draw(data->ctx, data->processed_draw_command);
return true;
}
void gdraw_command_list_draw(GContext *ctx, GDrawCommandList *command_list) {
gdraw_command_list_draw_processed(ctx, command_list, NULL);
}
static bool prv_iterate_max_command_size(GDrawCommand *command, uint32_t idx, void *ctx) {
size_t *size = ctx;
const size_t command_size = gdraw_command_get_data_size(command);
if (command_size > *size) {
*size = command_size;
}
return true;
}
T_STATIC size_t prv_get_list_max_command_size(GDrawCommandList *command_list) {
if (!command_list) {
return 0;
}
size_t size = 0;
gdraw_command_list_iterate(command_list, prv_iterate_max_command_size, &size);
return size;
}
void gdraw_command_list_draw_processed(GContext *ctx, GDrawCommandList *command_list,
GDrawCommandProcessor *processor) {
if (!ctx || !command_list) {
return;
}
if (!processor) {
gdraw_command_list_iterate(command_list, prv_draw_command, ctx);
} else {
const size_t max_size = prv_get_list_max_command_size(command_list);
GDrawCommandDrawProcessedCBData data = {
.ctx = ctx,
.list = command_list,
.processor = processor,
// malloc because we clear the memory within each iteration of `prv_draw_command_processed`
.processed_draw_command = applib_malloc(max_size)
};
if (data.processed_draw_command) {
gdraw_command_list_iterate(command_list, prv_draw_command_processed, &data);
applib_free(data.processed_draw_command);
}
}
}
static bool prv_calc_size(GDrawCommand *command, uint32_t idx, void *ctx) {
size_t *size = ctx;
*size += gdraw_command_get_data_size(command);
return true;
}
uint32_t gdraw_command_list_get_num_commands(GDrawCommandList *command_list) {
if (!command_list) {
return 0;
}
return command_list->num_commands;
}
size_t gdraw_command_list_get_data_size(GDrawCommandList *command_list) {
if (!command_list) {
return 0;
}
size_t size = sizeof(GDrawCommandList);
gdraw_command_list_iterate(command_list, prv_calc_size, &size);
return size;
}
static bool prv_get_num_points(GDrawCommand *command, uint32_t idx, void *ctx) {
size_t *num_gpoints = ctx;
*num_gpoints += gdraw_command_get_num_points(command);
return true;
}
size_t gdraw_command_list_get_num_points(GDrawCommandList *command_list) {
size_t num_gpoints = 0;
gdraw_command_list_iterate(command_list, prv_get_num_points, &num_gpoints);
return num_gpoints;
}
typedef struct {
const struct {
GPoint *points;
bool is_precise;
} values;
struct {
uint32_t current_index;
size_t bytes_left;
} iter;
} CollectPointsCBContext;
_Static_assert((sizeof(GPoint) == sizeof(GPointPrecise)),
"GPointPrecise cannot be convert to GPoint in-place because of its size difference.");
_Static_assert((offsetof(GPoint, y) == offsetof(GPointPrecise, y)),
"GPointPrecise cannot be convert to GPoint in-place because of its member size difference.");
static bool prv_collect_points(GDrawCommand *command, uint32_t idx, void *ctx) {
CollectPointsCBContext *collect = ctx;
const size_t bytes_copied = gdraw_command_copy_points(command,
&collect->values.points[collect->iter.current_index], collect->iter.bytes_left);
const uint16_t num_copied = bytes_copied / sizeof(GPoint);
// convert to regular GPoint
if (command->type == GDrawCommandTypePrecisePath && !collect->values.is_precise) {
for (uint16_t i = 0; i < num_copied; i++) {
GPoint *point_buffer = &collect->values.points[collect->iter.current_index + i];
GPointPrecise point = *(GPointPrecise *)point_buffer;
*point_buffer = GPointFromGPointPrecise(point);
}
}
// convert to GPointPrecise
else if (command->type == GDrawCommandTypePath && collect->values.is_precise) {
for (uint16_t i = 0; i < num_copied; i++) {
GPoint *point_buffer = &collect->values.points[collect->iter.current_index + i];
GPoint point = *point_buffer;
*(GPointPrecise *)point_buffer = GPointPreciseFromGPoint(point);
}
}
collect->iter.current_index += num_copied;
collect->iter.bytes_left -= bytes_copied;
return true;
}
GPoint *gdraw_command_list_collect_points(GDrawCommandList *command_list, bool is_precise,
uint16_t *num_points_out) {
const uint16_t num_points = gdraw_command_list_get_num_points(command_list);
const size_t max_bytes = num_points * sizeof(GPoint);
GPoint *points = applib_malloc(num_points * sizeof(GPoint));
if (!points) {
return NULL;
}
CollectPointsCBContext ctx = {
.values = {
.points = points,
.is_precise = is_precise,
},
.iter.bytes_left = max_bytes,
};
gdraw_command_list_iterate(command_list, prv_collect_points, &ctx);
if (num_points_out) {
*num_points_out = num_points;
}
return points;
}

View file

@ -0,0 +1,137 @@
/*
* 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.
*/
#pragma once
#include "gdraw_command.h"
#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include <stdint.h>
#include <stdbool.h>
//! @file graphics/gdraw_command_list.h
//! Defines the functions to manipulate \ref GDrawCommandList objects
//! @addtogroup Graphics
//! @{
//! @addtogroup DrawCommand Draw Commands
//! @{
struct GDrawCommandList;
//! Draw command lists contain a list of commands that can be iterated over and drawn all at once
typedef struct GDrawCommandList GDrawCommandList;
typedef struct GDrawCommandProcessor GDrawCommandProcessor;
//! Callback for iterating over GDrawCommands
//! @param processor GDrawCommandProcessor that is currently iterating over the GDrawCommandList.
//! @param proccessed_command Copy of the current GDrawCommand that can be modified
//! @param processed_command_max_size Size of GDrawCommand being processed
//! @param list list of GDrawCommands that will be modified by the processor
//! @param command Current GDrawCommand being processed
typedef void (*GDrawCommandProcessCommand)(GDrawCommandProcessor *processor,
GDrawCommand *processed_command,
size_t processed_command_max_size,
const GDrawCommandList* list,
const GDrawCommand *command);
//! @internal
//! Data used by the processor
typedef struct GDrawCommandProcessor {
// TODO: PBL-23778 processors for image, sequence, frame
GDrawCommandProcessCommand command;
} GDrawCommandProcessor;
//! Callback for iterating over draw command list
//! @param command current \ref GDrawCommand in iteration
//! @param index index of the current command in the list
//! @param context context pointer for the iteration operation
//! @return true if the iteration should continue after this command is processed
typedef bool (*GDrawCommandListIteratorCb)(GDrawCommand *command, uint32_t index, void *context);
//! @internal
//! Use to validate a command list read from flash or copied from serialized data
//! @param size Size of the command list structure in memory, in bytes
bool gdraw_command_list_validate(GDrawCommandList *command_list, size_t size);
//! @internal
//! Iterate over all commands in a command list
//! @param command_list \ref GDrawCommandList over which to iterate
//! @param handle_command iterator callback
//! @param callback_context context pointer to be passed into the iterator callback
//! @returns pointer to the address immediately following the end of the command list
void *gdraw_command_list_iterate_private(GDrawCommandList *command_list,
GDrawCommandListIteratorCb handle_command,
void *callback_context);
//! Iterate over all commands in a command list
//! @param command_list \ref GDrawCommandList over which to iterate
//! @param handle_command iterator callback
//! @param callback_context context pointer to be passed into the iterator callback
void gdraw_command_list_iterate(GDrawCommandList *command_list,
GDrawCommandListIteratorCb handle_command, void *callback_context);
//! Draw all commands in a command list
//! @param ctx The destination graphics context in which to draw
//! @param command_list list of commands to draw
void gdraw_command_list_draw(GContext *ctx, GDrawCommandList *command_list);
//! Process and draw all commands in a command list
//! @param ctx The destination graphics context in which to draw
//! @param command_list list of commands to draw
//! @param processor Command processor required for drawing processed commands
void gdraw_command_list_draw_processed(GContext *ctx, GDrawCommandList *command_list,
GDrawCommandProcessor *processor);
//! Get the command at the specified index
//! @note the specified index must be less than the number of commands in the list
//! @param command_list \ref GDrawCommandList from which to get a command
//! @param command_idx index of the command to get
//! @return pointer to \ref GDrawCommand at the specified index
GDrawCommand *gdraw_command_list_get_command(GDrawCommandList *command_list, uint16_t command_idx);
//! Get the number of commands in the list
//! @param command_list \ref GDrawCommandList from which to get the number of commands
//! @return number of commands in command list
uint32_t gdraw_command_list_get_num_commands(GDrawCommandList *command_list);
//! @internal
//! Get the total number of points in the list among all GDrawCommands
size_t gdraw_command_list_get_num_points(GDrawCommandList *command_list);
//! @internal
//! Get the size of a list in memory
size_t gdraw_command_list_get_data_size(GDrawCommandList *command_list);
//! @internal
//! Collect all the points in the draw commands list into a newly allocated buffer
//! The order is guaranteed to be the definition order of the points
//! @param command_list \ref GDrawCommandList from which to collect points
//! @param is_precise true to convert to GPointPrecise, otherwise points are converted to GPoint
//! @param num_points_out Optinal pointer to uint16_t to receive the num points
GPoint *gdraw_command_list_collect_points(GDrawCommandList *command_list, bool is_precise,
uint16_t *num_points_out);
bool gdraw_command_list_copy(void *buffer, size_t buffer_length, GDrawCommandList *src);
GDrawCommandList *gdraw_command_list_clone(GDrawCommandList *list);
void gdraw_command_list_destroy(GDrawCommandList *list);
//! @} // end addtogroup DrawCommand
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,94 @@
/*
* 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.
*/
#pragma once
#include "gdraw_command.h"
#include "gdraw_command_list.h"
#include "gdraw_command_image.h"
#include "gdraw_command_frame.h"
#include "gdraw_command_sequence.h"
#include "applib/graphics/gtypes.h"
#include "util/pack.h"
#include <stdint.h>
#include <stdbool.h>
#define GDRAW_COMMAND_VERSION (1)
#define PDCS_SIGNATURE MAKE_WORD('P', 'D', 'C', 'S')
#define PDCS_SIZE_OFFSET sizeof(PDCS_SIGNATURE)
#define PDCS_DATA_OFFSET (PDCS_SIZE_OFFSET + sizeof(uint32_t))
#define PDCI_SIGNATURE MAKE_WORD('P', 'D', 'C', 'I')
#define PDCI_SIZE_OFFSET sizeof(PDCI_SIGNATURE)
#define PDCI_DATA_OFFSET (PDCI_SIZE_OFFSET + sizeof(uint32_t))
struct __attribute__((__packed__)) GDrawCommand {
GDrawCommandType type:8;
struct {
uint8_t hidden:1;
uint8_t reserved:7;
};
GColor stroke_color;
uint8_t stroke_width;
GColor fill_color;
union {
struct { // path
bool path_open;
};
struct { // circle
uint16_t radius;
};
};
union {
struct {
uint16_t num_points;
GPoint points[];
};
struct {
uint16_t num_precise_points;
GPointPrecise precise_points[];
};
};
};
struct __attribute__((__packed__)) GDrawCommandList {
uint16_t num_commands;
GDrawCommand commands[];
};
struct __attribute__((__packed__)) GDrawCommandImage {
uint8_t version;
uint8_t reserved;
GSize size;
GDrawCommandList command_list;
};
struct __attribute__((__packed__)) GDrawCommandFrame {
uint16_t duration;
GDrawCommandList command_list;
};
struct __attribute__((__packed__)) GDrawCommandSequence {
uint8_t version;
uint8_t reserved;
GSize size;
uint16_t play_count;
uint16_t num_frames;
GDrawCommandFrame frames[];
};

View file

@ -0,0 +1,216 @@
/*
* 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 "gdraw_command_sequence.h"
#include "gdraw_command_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/applib_resource_private.h"
#include "syscall/syscall.h"
#define GDRAW_COMMAND_SEQUENCE_PLAY_COUNT_INFINITE_STORED ((uint16_t) ~0)
static GDrawCommandFrame *prv_next_frame(GDrawCommandFrame *frame) {
// Iterate to the end of the command list (next frame starts immediately afterwards)
return gdraw_command_list_iterate_private(&frame->command_list, NULL, NULL);
}
GDrawCommandSequence *gdraw_command_sequence_create_with_resource(uint32_t resource_id) {
ResAppNum app_num = sys_get_current_resource_num();
return gdraw_command_sequence_create_with_resource_system(app_num, resource_id);
}
GDrawCommandSequence *gdraw_command_sequence_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id) {
uint32_t data_size;
if (!gdraw_command_resource_is_valid(app_num, resource_id, PDCS_SIGNATURE, &data_size)) {
return NULL;
}
GDrawCommandSequence *draw_command_sequence = applib_resource_mmap_or_load(app_num, resource_id,
PDCS_DATA_OFFSET,
data_size, false);
// Validate the loaded command sequence
if (!gdraw_command_sequence_validate(draw_command_sequence, data_size)) {
gdraw_command_sequence_destroy(draw_command_sequence);
return NULL;
}
return draw_command_sequence;
}
GDrawCommandSequence *gdraw_command_sequence_clone(GDrawCommandSequence *sequence) {
if (!sequence) {
return NULL;
}
// potentially extracting into a generic task_ptrdup(void *, size_t)
size_t size = gdraw_command_sequence_get_data_size(sequence);
GDrawCommandSequence *result = applib_malloc(size);
if (result) {
memcpy(result, sequence, size);
}
return result;
}
void gdraw_command_sequence_destroy(GDrawCommandSequence *sequence) {
applib_resource_munmap_or_free(sequence);
}
bool gdraw_command_sequence_validate(GDrawCommandSequence *sequence, size_t size) {
if (!sequence ||
(size < sizeof(GDrawCommandSequence)) ||
(sequence->version > GDRAW_COMMAND_VERSION) ||
(sequence->num_frames == 0)) {
return false;
}
uint8_t *end = (uint8_t *)sequence + size;
GDrawCommandFrame *frame = sequence->frames;
for (uint32_t i = 0; i < sequence->num_frames; i++) {
if (((uint8_t *) frame >= end) ||
!gdraw_command_frame_validate(frame, (size_t)(end - (uint8_t *)frame))) {
return false;
}
frame = prv_next_frame(frame);
}
return (end == (uint8_t *) frame);
}
static uint32_t prv_get_single_play_duration(GDrawCommandSequence *sequence) {
uint32_t total = 0;
GDrawCommandFrame *frame = sequence->frames;
for (uint32_t i = 0; i < sequence->num_frames; i++) {
total += gdraw_command_frame_get_duration(frame);
frame = prv_next_frame(frame);
}
return total;
}
GDrawCommandFrame *gdraw_command_sequence_get_frame_by_elapsed(GDrawCommandSequence *sequence,
uint32_t elapsed) {
if (!sequence) {
return NULL;
}
if ((sequence->play_count != GDRAW_COMMAND_SEQUENCE_PLAY_COUNT_INFINITE_STORED) &&
(elapsed >= gdraw_command_sequence_get_total_duration(sequence))) {
// return the last frame if the elapsed time is longer than the total duration
return gdraw_command_sequence_get_frame_by_index(sequence, sequence->num_frames - 1);
}
elapsed %= prv_get_single_play_duration(sequence);
uint32_t total = 0;
GDrawCommandFrame *frame = sequence->frames;
for (uint32_t i = 0; i < sequence->num_frames; i++) {
total += gdraw_command_frame_get_duration(frame);
if (total > elapsed) {
break;
}
frame = prv_next_frame(frame);
}
// return the last frame in the sequence if the elapsed time is longer than the total time of the
// sequence
return frame;
}
GDrawCommandFrame *gdraw_command_sequence_get_frame_by_index(GDrawCommandSequence *sequence,
uint32_t index) {
if (!sequence || (index >= sequence->num_frames)) {
return NULL;
}
GDrawCommandFrame *frame = sequence->frames;
for (uint32_t i = 0; i < index; i++) {
frame = prv_next_frame(frame);
}
return frame;
}
size_t gdraw_command_sequence_get_data_size(GDrawCommandSequence *sequence) {
if (!sequence) {
return 0;
}
size_t size = sizeof(GDrawCommandSequence);
GDrawCommandFrame *frame = sequence->frames;
for (uint32_t i = 0; i < sequence->num_frames; i++) {
size += gdraw_command_frame_get_data_size(frame);
frame = prv_next_frame(frame);
}
return size;
}
GSize gdraw_command_sequence_get_bounds_size(GDrawCommandSequence *sequence) {
if (!sequence) {
return GSizeZero;
}
return sequence->size;
}
void gdraw_command_sequence_set_bounds_size(GDrawCommandSequence *sequence, GSize size) {
if (!sequence) {
return;
}
sequence->size = size;
}
uint32_t gdraw_command_sequence_get_play_count(GDrawCommandSequence *sequence) {
if (!sequence) {
return 0;
}
if (sequence->play_count == GDRAW_COMMAND_SEQUENCE_PLAY_COUNT_INFINITE_STORED) {
return PLAY_COUNT_INFINITE;
}
return sequence->play_count;
}
void gdraw_command_sequence_set_play_count(GDrawCommandSequence *sequence, uint32_t play_count) {
if (!sequence) {
return;
}
sequence->play_count = MIN(play_count, GDRAW_COMMAND_SEQUENCE_PLAY_COUNT_INFINITE_STORED);
}
uint32_t gdraw_command_sequence_get_total_duration(GDrawCommandSequence *sequence) {
if (!sequence) {
return 0;
}
if (sequence->play_count == GDRAW_COMMAND_SEQUENCE_PLAY_COUNT_INFINITE_STORED) {
return PLAY_DURATION_INFINITE;
}
return prv_get_single_play_duration(sequence) * sequence->play_count;
}
uint32_t gdraw_command_sequence_get_num_frames(GDrawCommandSequence *sequence) {
if (!sequence) {
return 0;
}
return sequence->num_frames;
}

View file

@ -0,0 +1,118 @@
/*
* 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.
*/
#pragma once
#include "gdraw_command_frame.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/gtypes.h"
#include <stdint.h>
#include <stdbool.h>
//! @file graphics/gdraw_command_sequence.h
//! Defines the functions to manipulate \ref GDrawCommandSequence objects
//! @addtogroup Graphics
//! @{
//! @addtogroup DrawCommand Draw Commands
//! @{
struct GDrawCommandSequence;
//! Draw command sequences allow the animation of frames over time. Each sequence has a list of
//! frames that can be accessed by the elapsed duration of the animation (not maintained internally)
//! or by index. Sequences can be loaded from PDC file data.
typedef struct GDrawCommandSequence GDrawCommandSequence;
//! Creates a \ref GDrawCommandSequence from the specified resource (PDC file)
//! @param resource_id Resource containing data to load and create GDrawCommandSequence from.
//! @return GDrawCommandSequence pointer if the resource was loaded, NULL otherwise
GDrawCommandSequence *gdraw_command_sequence_create_with_resource(uint32_t resource_id);
//! @internal
GDrawCommandSequence *gdraw_command_sequence_create_with_resource_system(ResAppNum app_num,
uint32_t resource_id);
//! Creates a \ref GDrawCommandSequence as a copy from a given sequence
//! @param sequence Sequence to copy
//! @return cloned sequence or NULL if the operation failed
GDrawCommandSequence *gdraw_command_sequence_clone(GDrawCommandSequence *sequence);
//! Deletes the \ref GDrawCommandSequence structure and frees associated data
//! @param image Pointer to the sequence to destroy
void gdraw_command_sequence_destroy(GDrawCommandSequence *sequence);
//! @internal
//! Use to validate a sequence read from flash or copied from serialized data
//! @param size Size of the sequence in memory, in bytes
bool gdraw_command_sequence_validate(GDrawCommandSequence *sequence, size_t size);
//! Get the frame that should be shown after the specified amount of elapsed time
//! The last frame will be returned if the elapsed time exceeds the total time
//! @param sequence \ref GDrawCommandSequence from which to get the frame
//! @param elapsed_ms elapsed time in milliseconds
//! @return pointer to \ref GDrawCommandFrame that should be displayed at the elapsed time
GDrawCommandFrame *gdraw_command_sequence_get_frame_by_elapsed(GDrawCommandSequence *sequence,
uint32_t elapsed_ms);
//! Get the frame at the specified index
//! @param sequence \ref GDrawCommandSequence from which to get the frame
//! @param index Index of frame to get
//! @return pointer to \ref GDrawCommandFrame at the specified index
GDrawCommandFrame *gdraw_command_sequence_get_frame_by_index(GDrawCommandSequence *sequence,
uint32_t index);
//! @internal
//! Get the size, in bytes, of the sequence in memory
size_t gdraw_command_sequence_get_data_size(GDrawCommandSequence *sequence);
//! Get the size of the bounding box surrounding all draw commands in the sequence. This bounding
//! box can be used to set the graphics context or layer bounds when drawing the frames in the
//! sequence.
//! @param sequence \ref GDrawCommandSequence from which to get the bounds
//! @return bounding box size
GSize gdraw_command_sequence_get_bounds_size(GDrawCommandSequence *sequence);
//! Set size of the bounding box surrounding all draw commands in the sequence. This bounding
//! box can be used to set the graphics context or layer bounds when drawing the frames in the
//! sequence.
//! @param sequence \ref GDrawCommandSequence for which to set the bounds
//! @param size bounding box size
void gdraw_command_sequence_set_bounds_size(GDrawCommandSequence *sequence, GSize size);
//! Get the play count of the sequence
//! @param sequence \ref GDrawCommandSequence from which to get the play count
//! @return play count of sequence
uint32_t gdraw_command_sequence_get_play_count(GDrawCommandSequence *sequence);
//! Set the play count of the sequence
//! @param sequence \ref GDrawCommandSequence for which to set the play count
//! @param play_count play count
void gdraw_command_sequence_set_play_count(GDrawCommandSequence *sequence, uint32_t play_count);
//! Get the total duration of the sequence.
//! @param sequence \ref GDrawCommandSequence from which to get the total duration
//! @return total duration of the sequence in milliseconds
uint32_t gdraw_command_sequence_get_total_duration(GDrawCommandSequence *sequence);
//! Get the number of frames in the sequence
//! @param sequence \ref GDrawCommandSequence from which to get the number of frames
//! @return number of frames in the sequence
uint32_t gdraw_command_sequence_get_num_frames(GDrawCommandSequence *sequence);
//! @} // end addtogroup DrawCommand
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,491 @@
/*
* 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 "gdraw_command_transforms.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/gdraw_command_private.h"
#include "util/trig.h"
#include "applib/ui/animation.h"
#include "applib/ui/animation_interpolate.h"
#include "applib/ui/animation_timing.h"
#include "system/passert.h"
#include "util/math_fixed.h"
#include <stdio.h>
////////////////////
// scale
typedef struct {
GSize from;
GSize to;
} ScaleCBContext;
T_STATIC bool prv_gdraw_command_scale(GDrawCommand *command, uint32_t index, void *context) {
ScaleCBContext *scale = context;
const uint16_t num_points = gdraw_command_get_num_points(command);
for (uint16_t i = 0; i < num_points; i++) {
command->points[i] = gpoint_scale_by_gsize(command->points[i], scale->from, scale->to);
}
return true;
}
void gdraw_command_list_scale(GDrawCommandList *list, GSize from, GSize to) {
ScaleCBContext ctx = {
.from = from,
.to = to,
};
gdraw_command_list_iterate(list, prv_gdraw_command_scale, &ctx);
}
void gdraw_command_image_scale(GDrawCommandImage *image, GSize to) {
gdraw_command_list_scale(&image->command_list, image->size, to);
image->size = to;
}
////////////////////
// attract to square
static int16_t prv_int_attract_to(int16_t value, int16_t bounds, int32_t normalized) {
const int16_t delta_0 = (int16_t) ((0 + 1) - value);
const int16_t delta_b = (int16_t) ((bounds - 1) - value);
const int16_t delta = ABS(delta_0) < ABS(delta_b) ? delta_0 : delta_b;
return (int16_t) (value + delta * normalized / ANIMATION_NORMALIZED_MAX);
}
GPoint gpoint_attract_to_square(GPoint point, GSize size, int32_t normalized) {
// hacky to square - TODO: implement for real
point.y += 1;
point = GPoint(
prv_int_attract_to(point.x, size.w, normalized),
prv_int_attract_to(point.y, size.h, normalized));
return point;
}
typedef struct {
GSize integer_size;
GSize precise_size;
int32_t normalized;
} ToSquareCBContext;
T_STATIC bool prv_gdraw_command_attract_to_square(GDrawCommand *command, uint32_t index,
void *context) {
ToSquareCBContext *to_square = context;
const uint16_t num_points = gdraw_command_get_num_points(command);
for (uint16_t i = 0; i < num_points; i++) {
const GSize size = (command->type == GDrawCommandTypePrecisePath)
? to_square->precise_size : to_square->integer_size;
command->points[i] = gpoint_attract_to_square(command->points[i],
size, to_square->normalized);
}
return true;
}
void gdraw_command_list_attract_to_square(GDrawCommandList *list, GSize size, int32_t normalized) {
ToSquareCBContext ctx = {
.integer_size = size,
.precise_size = gsize_scalar_lshift(size, GPOINT_PRECISE_PRECISION),
.normalized = normalized,
};
gdraw_command_list_iterate(list, prv_gdraw_command_attract_to_square, &ctx);
}
void gdraw_command_image_attract_to_square(GDrawCommandImage *image, int32_t normalized) {
gdraw_command_list_attract_to_square(&image->command_list, image->size, normalized);
}
////////////////////
// gpoint index lookup creator
typedef struct {
const struct {
const GPoint *points;
uint16_t num_points;
} values;
struct {
GPointIndexLookup *lookup;
uint32_t current_index;
} iter;
} GPointCreateIndexCBContext;
T_STATIC bool prv_gdraw_command_create_point_index_lookup(GDrawCommand *command, uint32_t index,
void *context) {
GPointCreateIndexCBContext *lookup = context;
const uint16_t num_points = gdraw_command_get_num_points(command);
for (uint16_t i = 0; i < num_points; i++) {
GPoint point = command->points[i];
if (command->type == GDrawCommandTypePrecisePath) {
point = gpoint_scalar_rshift(point, GPOINT_PRECISE_PRECISION);
}
const uint32_t lookup_length = lookup->values.num_points;
for (uint16_t j = 0; j < lookup_length; j++) {
if (gpoint_equal(&point, &lookup->values.points[j])) {
lookup->iter.lookup->index_lookup[lookup->iter.current_index] = j;
break;
}
}
lookup->iter.current_index++;
}
return true;
}
GPointIndexLookup *gdraw_command_list_create_index_lookup(GDrawCommandList *list,
GPointComparator comparator, void *context, bool reverse) {
uint16_t num_points = 0;
const bool is_precise = false;
GPoint * const points = gdraw_command_list_collect_points(list, is_precise, &num_points);
if (!points) {
return NULL;
}
gpoint_sort(points, num_points, comparator, context, reverse);
GPointIndexLookup *lookup = applib_malloc(sizeof(GPointIndexLookup)
+ num_points * sizeof(uint16_t));
if (!lookup) {
applib_free(points);
return lookup;
}
lookup->num_points = num_points;
lookup->max_index = num_points - 1;
GPointCreateIndexCBContext ctx = {
.values = {
.points = points,
.num_points = num_points,
},
.iter = {
.lookup = lookup,
},
};
gdraw_command_list_iterate(list, prv_gdraw_command_create_point_index_lookup, &ctx);
applib_free(points);
return lookup;
}
typedef struct {
const GPoint origin;
const int32_t angle;
} AngleComparatorContext;
static int prv_angle_comparator(const GPoint * const a, const GPoint * const b,
void *context) {
AngleComparatorContext *ctx = context;
const int16_t angle_a = ABS(positive_modulo(
(atan2_lookup(a->y - ctx->origin.y, a->x - ctx->origin.x) + ctx->angle), TRIG_MAX_ANGLE) -
TRIG_MAX_ANGLE / 2);
const int16_t angle_b = ABS(positive_modulo(
(atan2_lookup(b->y - ctx->origin.y, b->x - ctx->origin.x) + ctx->angle), TRIG_MAX_ANGLE) -
TRIG_MAX_ANGLE / 2);
return (angle_a > angle_b ? 1 : -1);
}
GPointIndexLookup *gdraw_command_list_create_index_lookup_by_angle(GDrawCommandList *list,
GPoint origin, int32_t angle) {
AngleComparatorContext ctx = {
.origin = origin,
.angle = angle,
};
return gdraw_command_list_create_index_lookup(list, prv_angle_comparator, &ctx, false);
}
static int prv_distance_comparator(const GPoint * const a, const GPoint * const b,
void *context) {
const GPoint * const target = context;
uint32_t distance_a = gpoint_distance_squared(*a, *target);
uint32_t distance_b = gpoint_distance_squared(*b, *target);
return (distance_a > distance_b ? 1 : -1);
}
GPointIndexLookup *gdraw_command_list_create_index_lookup_by_distance(GDrawCommandList *list,
GPoint target) {
return gdraw_command_list_create_index_lookup(list, prv_distance_comparator, &target, false);
}
void gpoint_index_lookup_add_at(GPointIndexLookup *lookup, int delay_index, int delay_amount) {
if (delay_index < 0 || delay_index >= lookup->max_index) {
return;
}
// We are adding additional delay, the max delay index increases
lookup->max_index += delay_amount;
for (int i = 0; i < lookup->num_points; i++) {
// The lookup maps definition index => delay index
// We want to add delay to points at or above a certain delay index
if (lookup->index_lookup[i] >= delay_index) {
lookup->index_lookup[i] += delay_amount;
}
}
}
void gpoint_index_lookup_set_groups(GPointIndexLookup *lookup, int num_groups,
Fixed_S32_16 group_delay) {
const int num_points_per_group = lookup->num_points / num_groups;
const int delay_per_group = (num_points_per_group / group_delay.raw_value) /
FIXED_S32_16_ONE.raw_value;
const int group_delay_amount = num_points_per_group + delay_per_group;
for (uint16_t i = num_groups - 1; i >= 1; i--) {
gpoint_index_lookup_add_at(lookup, (i * num_points_per_group), group_delay_amount);
}
}
////////////////////
// segmented scale: index based segmentation of scale + transform
static int16_t prv_int_scale_to(
int16_t value, int16_t size, int16_t from_range, int16_t to_range, int32_t normalized,
InterpolateInt64Function interpolate) {
return value + ((int32_t) value * interpolate(
normalized, from_range - size, to_range - size)) / size;
}
T_STATIC int16_t prv_int_scale_and_translate_to(
int16_t value, int16_t size, int16_t from_range, int16_t to_range,
int16_t from_min, int16_t to_min, int32_t normalized, InterpolateInt64Function interpolate) {
const int32_t scale = prv_int_scale_to(value, size, from_range, to_range, normalized,
interpolate);
const int32_t translate = interpolate(normalized, from_min, to_min);
return scale + translate;
}
GPoint gpoint_scale_to(GPoint point, GSize size, GRect from, GRect to, int32_t normalized,
InterpolateInt64Function interpolate) {
return GPoint(
prv_int_scale_and_translate_to(point.x, size.w, from.size.w, to.size.w,
from.origin.x, to.origin.x, normalized, interpolate),
prv_int_scale_and_translate_to(point.y, size.h, from.size.h, to.size.h,
from.origin.y, to.origin.y, normalized, interpolate));
}
typedef struct {
GRect from;
GRect to;
GSize size;
GPoint offset;
} ScaleToGValues;
typedef struct {
const struct {
ScaleToGValues integer;
ScaleToGValues precise;
Fixed_S32_16 duration_fraction;
GPointIndexLookup *lookup;
AnimationProgress normalized;
InterpolateInt64Function interpolate;
bool is_offset;
} values;
struct {
uint32_t current_index;
} iter;
} ScaleToCBContext;
T_STATIC int64_t prv_default_interpolate(int32_t normalized, int64_t from, int64_t to) {
const int32_t curved = animation_timing_curve(normalized, AnimationCurveEaseInOut);
return interpolate_int64_linear(curved, from, to);
}
T_STATIC bool prv_gdraw_command_scale_segmented(GDrawCommand *command, uint32_t index,
void *context) {
ScaleToCBContext *scale = context;
const ScaleToGValues * const gvalues = (command->type == GDrawCommandTypePrecisePath)
? &scale->values.precise : &scale->values.integer;
const uint16_t num_points = gdraw_command_get_num_points(command);
for (uint16_t i = 0; i < num_points; i++) {
const int32_t point_index = scale->values.lookup->index_lookup[scale->iter.current_index];
GPoint point = command->points[i];
if (scale->values.is_offset) {
gpoint_sub_eq(&point, gvalues->offset);
}
const AnimationProgress normalized = animation_timing_segmented(
scale->values.normalized, point_index, scale->values.lookup->max_index + 1,
scale->values.duration_fraction);
const InterpolateInt64Function interpolate = scale->values.interpolate ?
scale->values.interpolate : prv_default_interpolate;
point = gpoint_scale_to(point, gvalues->size, gvalues->from, gvalues->to, normalized,
interpolate);
if (scale->values.is_offset) {
gpoint_add_eq(&point, gvalues->offset);
}
command->points[i] = point;
scale->iter.current_index++;
}
return true;
}
void gdraw_command_list_scale_segmented_to(
GDrawCommandList *list, GSize size, GRect from, GRect to, AnimationProgress normalized,
InterpolateInt64Function interpolate, GPointIndexLookup *lookup, Fixed_S32_16 duration_fraction,
bool is_offset) {
GPoint offset = GPointZero;
if (is_offset) {
offset = from.origin;
to.origin = gpoint_sub(to.origin, from.origin);
from.origin = GPointZero;
}
ScaleToCBContext ctx = {
.values = {
.integer = {
.from = from,
.to = to,
.size = size,
.offset = offset,
},
.precise = {
.from = grect_scalar_lshift(from, GPOINT_PRECISE_PRECISION),
.to = grect_scalar_lshift(to, GPOINT_PRECISE_PRECISION),
.size = gsize_scalar_lshift(size, GPOINT_PRECISE_PRECISION),
.offset = gpoint_scalar_lshift(offset, GPOINT_PRECISE_PRECISION),
},
.duration_fraction = duration_fraction,
.lookup = lookup,
.normalized = normalized,
.interpolate = interpolate,
.is_offset = is_offset,
},
};
gdraw_command_list_iterate(list, prv_gdraw_command_scale_segmented, &ctx);
}
void gdraw_command_image_scale_segmented_to(
GDrawCommandImage *image, GRect from, GRect to, AnimationProgress normalized,
InterpolateInt64Function interpolate, GPointIndexLookup *lookup, Fixed_S32_16 duration_fraction,
bool is_offset) {
gdraw_command_list_scale_segmented_to(
&image->command_list, image->size, from, to, normalized, interpolate, lookup,
duration_fraction, is_offset);
image->size = to.size;
}
////////////////////
// scale stroke width
typedef struct {
Fixed_S16_3 from;
Fixed_S16_3 to;
AnimationProgress progress;
GStrokeWidthOp from_op;
GStrokeWidthOp to_op;
} ScaleStrokeWidthCBContext;
Fixed_S16_3 prv_stroke_width_transform(Fixed_S16_3 native, Fixed_S16_3 op_value,
GStrokeWidthOp op) {
switch (op) {
case GStrokeWidthOpSet:
return op_value;
case GStrokeWidthOpMultiply:
return Fixed_S16_3_mul(native, op_value);
case GStrokeWidthOpAdd:
return Fixed_S16_3_add(native, op_value);
default:
WTF;
}
}
static bool prv_gdraw_command_scale_stroke_width(GDrawCommand *command, uint32_t index,
void *context) {
ScaleStrokeWidthCBContext *scale = context;
const Fixed_S16_3 stroke_width =
Fixed_S16_3(gdraw_command_get_stroke_width(command) << FIXED_S16_3_PRECISION);
Fixed_S16_3 from_stroke_width = prv_stroke_width_transform(stroke_width, scale->from,
scale->from_op);
Fixed_S16_3 to_stroke_width = prv_stroke_width_transform(stroke_width, scale->to,
scale->to_op);
const uint16_t new_stroke_width = interpolate_int64_linear(
scale->progress, from_stroke_width.raw_value, to_stroke_width.raw_value);
gdraw_command_set_stroke_width(
command, ((new_stroke_width + FIXED_S16_3_HALF.raw_value) >> FIXED_S16_3_PRECISION));
return true;
}
void gdraw_command_list_scale_stroke_width(GDrawCommandList *list, Fixed_S16_3 from, Fixed_S16_3 to,
GStrokeWidthOp from_op, GStrokeWidthOp to_op,
AnimationProgress progress) {
ScaleStrokeWidthCBContext ctx = {
.from = from,
.to = to,
.from_op = from_op,
.to_op = to_op,
.progress = progress,
};
gdraw_command_list_iterate(list, prv_gdraw_command_scale_stroke_width, &ctx);
}
void gdraw_command_image_scale_stroke_width(GDrawCommandImage *image, Fixed_S16_3 from,
Fixed_S16_3 to, GStrokeWidthOp from_op,
GStrokeWidthOp to_op, AnimationProgress progress) {
gdraw_command_list_scale_stroke_width(&image->command_list, from, to, from_op, to_op, progress);
}
////////////////////
// replace color
typedef struct {
GColor from;
GColor to;
} ReplaceColorCBContext;
void gdraw_command_replace_color(GDrawCommand *command, GColor from, GColor to) {
if (gcolor_equal(from, command->fill_color)) {
command->fill_color = to;
}
if (gcolor_equal(from, command->stroke_color)) {
command->stroke_color = to;
}
}
bool prv_replace_color(GDrawCommand *command, uint32_t index, void *context) {
ReplaceColorCBContext *cb_context = context;
gdraw_command_replace_color(command, cb_context->from, cb_context->to);
return true;
}
void gdraw_command_list_replace_color(GDrawCommandList *list, GColor from, GColor to) {
ReplaceColorCBContext context = {
.from = from,
.to = to,
};
gdraw_command_list_iterate(list, prv_replace_color, &context);
}
void gdraw_command_frame_replace_color(GDrawCommandFrame *frame, GColor from, GColor to) {
gdraw_command_list_replace_color(&frame->command_list, from, to);
}

View file

@ -0,0 +1,187 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gdraw_command_image.h"
#include "applib/graphics/gdraw_command_sequence.h"
#include "applib/ui/animation_timing.h"
#include "util/math_fixed.h"
// GDraw Command Transforms is a collection of draw command transforms.
//
// Some transforms apply effects immediately and others are to be used in an animation.
// Transforms that are for animation and take a normalized position use the infinitive "to" as
// opposed to "animation" for brevity.
//
// Among the animation transforms, there is class that delays the animation for each of its
// participants with different delay times. These transforms are suffixed with "segmented" and
// generally time the points by using a combination of GPointIndexLookup and
// animation_timing_segmented.
//! GStrokeWidthOp specifies the different types of operations to perform during a stroke
//! width transform. Stroke width transformation takes a start and an end, so combining two
//! operators can result in your desired animation. Each operation is paired with a value to
//! operate along with the native stroke width. For example, if you want to start from a circle
//! of diameter 10px and transform to 2x the native stroke width, start with GStrokeWidthOpSet of 10
//! and end with GStrokeWidthOpMultiply of 2.
typedef enum {
//! Sets the stroke width to the paired operation value, overriding the native stroke width
GStrokeWidthOpSet,
//! Multiplies the native stroke width with the paired operation value, scaling the stroke width
GStrokeWidthOpMultiply,
//! Adds the paired operation value to the native stroke width
GStrokeWidthOpAdd,
} GStrokeWidthOp;
//! A GPointIndexLookup is used for segmented animations.
//! Segmented animations are where participating elements have a delayed start compared to other
//! elements in the same animation. Each element has the same animation time, so earlier elements
//! complete their animation sooner than others.
//! GPointIndexLookup is a lookup array with the mapping (GPoint index => animation index).
//! The animation index is used as the delay multiple in segmented animations.
//! The delay multiple is how many delay segments the particular GPoint must wait before it is
//! transformed.
//! @see animation_timing_segmented
typedef struct {
uint16_t max_index;
uint16_t num_points;
uint16_t index_lookup[];
} GPointIndexLookup;
//! Scales a list from one size to another
void gdraw_command_list_scale(GDrawCommandList *list, GSize from, GSize to);
//! Scales an image to a given size
void gdraw_command_image_scale(GDrawCommandImage *image, GSize to);
//! Attracts points of a list to a square
void gdraw_command_list_attract_to_square(GDrawCommandList *list, GSize size, int32_t normalized);
//! Attracts points of an image to a square
void gdraw_command_image_attract_to_square(GDrawCommandImage *image, int32_t normalized);
GPoint gpoint_attract_to_square(GPoint point, GSize size, int32_t normalized);
//! Creates a GPointIndexLookup based on the angle to the center of an image
//! Points in the image whose ray with the image's center has a smaller angle are animated first.
//! @param angle Angle at which to consider zero. Points at this angle animate first.
//! @see GPointIndexLookup
GPointIndexLookup *gdraw_command_list_create_index_lookup_by_angle(GDrawCommandList *list,
GPoint origin, int32_t angle);
//! Creates a GPointIndexLookup based on distance to a target GPoint.
//! Points in the image that are closer to the target are given the lowest animation index and
//! are therefore animated first.
//! To obtain a stretching animation, select a target among the points in a image's perimeter that
//! is most closest to its destination animation point.
//! Choosing a target in the image's perimeter opposite of the destination animation point results
//! in a paper flipping effect.
//! @param target Point to compare against in image coordinates. (0, 0) is top left.
//! @see GPointIndexLookup
GPointIndexLookup *gdraw_command_list_create_index_lookup_by_distance(GDrawCommandList *list,
GPoint target);
//! Shifts the delay index of all points at or above a given delay index.
//! \note This shifts the delay index up, so be sure to insert the last most delays first.
//! @param lookup GPointIndexLookup to add delay to
//! @param index Delay index to add delay to
//! @param amount Amount of delay to add in index units
//! @see GPointIndexLookup
void gpoint_index_lookup_add_at(GPointIndexLookup *lookup, int delay_index, int delay_amount);
//! Adds delay between the groups that the lookup is desired to be partitioned into. The groups
//! are partitioned evenly by number of points.
//! @param lookup GPointIndexLookup to add delay to
//! @param num_groups Number of groups to partition by
//! @param group_delay Amount of additional delay to add between each group proportional to the
//! animation duration of one group
//! @see GPointIndexLookup
void gpoint_index_lookup_set_groups(GPointIndexLookup *lookup, int num_groups,
Fixed_S32_16 group_delay);
//! Performs a scaling and translation transform on a list with each point being delayed by delay
//! segments assigned based on a GPointIndexLookup.
//! @param size Native size of the points within the command list. This is used for scaling.
//! @param from Position and size to start from in local drawing coordinates.
//! @param to Position and size to end at in local drawing coordinates.
//! @param normalized Normalized animation position to transform to.
//! @param interpolate InterpolateInt64Function to apply to each point individually
//! @param lookup \ref GPointIndexLookup delay index that each point's delay is derived from.
//! @param duration_fraction \ref animation_timing_segmented animation duration that each
//! point would animate in within the animation's duration.
//! @param is_offset true if the command list has already been offset another transform. When true,
//! this prevents the transform from scaling the translation already present in the command list
//! equivalent to `from.origin`.
//! @see GPointIndexLookup
void gdraw_command_list_scale_segmented_to(
GDrawCommandList *list, GSize size, GRect from, GRect to, AnimationProgress normalized,
InterpolateInt64Function interpolate, GPointIndexLookup *lookup, Fixed_S32_16 duration_fraction,
bool is_offset);
//! Performs a scaling and translation transform on an image with each point being delayed by delay
//! segments assigned based on a GPointIndexLookup.
//! @param from Position and size to start from in local drawing coordinates.
//! @param to Position and size to end at from in local drawing coordinates.
//! @param normalized Normalized animation position to transform to.
//! @param interpolate InterpolateInt64Function to apply to each point individually
//! @param lookup \ref GPointIndexLookup delay index that each point's delay is derived from.
//! @param duration_fraction \ref animation_timing_segmented animation duration that each
//! point would animate in within the animation's duration.
//! @param is_offset true if the command list has already been offset another transform. When true,
//! this prevents the transform from scaling the translation already present in the command list
//! equivalent to `from.origin`.
//! @see GPointIndexLookup
void gdraw_command_image_scale_segmented_to(
GDrawCommandImage *image, GRect from, GRect to, AnimationProgress normalized,
InterpolateInt64Function interpolate, GPointIndexLookup *lookup, Fixed_S32_16 duration_fraction,
bool is_offset);
//! Scales and translates a GPoint.
//! @param point Point to transform.
//! @param size Dimensions of the canvas or image the point belongs to.
//! @param from Position and size to start from in local drawing coordinates.
//! @param to Position and size to end at from in local drawing coordinates.
//! @param normalized Normalized animation position to transform to.
//! @param interpolate InterpolateInt64Function to use for interpolation.
GPoint gpoint_scale_to(GPoint point, GSize size, GRect from, GRect to, int32_t normalized,
InterpolateInt64Function interpolate);
//! Transforms the stroke width of a list as defined by a pair of GStrokeWidthOp.
//! @param list GDrawCommandList to scale stroke width of
//! @param from Fixed_S16_3 From stroke width operator value
//! @param to Fixed_S16_3 To stroke width operator value
//! @param from_op GStrokeWidthOp operation to start with
//! @param to_op GStrokeWidthOp operation to end with
//! @param progress AnimationProgress position of the transform
//! @see GStrokeWidthOp
void gdraw_command_list_scale_stroke_width(GDrawCommandList *list, Fixed_S16_3 from, Fixed_S16_3 to,
GStrokeWidthOp from_op, GStrokeWidthOp to_op,
AnimationProgress progress);
//! Transforms the stroke width of an image as defined by a pair of GStrokeWidthOp.
//! @param image GDrawCommandImage to scale stroke width of
//! @param from Fixed_S16_3 From stroke width operator value
//! @param to Fixed_S16_3 To stroke width operator value
//! @param from_op GStrokeWidthOp operation to start with
//! @param to_op GStrokeWidthOp operation to end with
//! @param progress AnimationProgress position of the transform
//! @see GStrokeWidthOp
void gdraw_command_image_scale_stroke_width(GDrawCommandImage *image, Fixed_S16_3 from,
Fixed_S16_3 to, GStrokeWidthOp from_op,
GStrokeWidthOp to_op, AnimationProgress progress);
void gdraw_command_frame_replace_color(GDrawCommandFrame *frame, GColor from, GColor to);
void gdraw_command_replace_color(GDrawCommand *command, GColor from, GColor to);

View file

@ -0,0 +1,586 @@
/*
* 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 "gpath.h"
#include "graphics.h"
#include "graphics_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/app_logging.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/swap.h"
#include "util/trig.h"
#include <string.h>
#include <stdlib.h>
#define GPATH_ERROR "Unable to allocate memory for GPath call"
void prv_fill_path_with_cb_aa(GContext *ctx, GPath *path, GPathDrawFilledCallback cb,
void *user_data);
typedef struct Intersection {
Fixed_S16_3 x;
Fixed_S16_3 delta;
} Intersection;
void gpath_init(GPath *path, const GPathInfo *init) {
memset(path, 0, sizeof(GPath));
path->num_points = init->num_points;
path->points = init->points;
}
GPath* gpath_create(const GPathInfo *init) {
// Can't pad this out because the definition itself is exported. Even if we did pad it out so
// we can theoretically add members to the end of the struct, we'll still have to add compatibilty
// flags throughout here to check which size of struct the app is going to pass us through these
// APIs.
GPath* path = applib_malloc(sizeof(GPath));
if (path) {
gpath_init(path, init);
} else {
APP_LOG(APP_LOG_LEVEL_ERROR, GPATH_ERROR);
}
return path;
}
void gpath_destroy(GPath* gpath) {
applib_free(gpath);
}
static GPoint rotate_offset_point(const GPoint *orig, int32_t rotation, const GPoint *offset) {
int32_t cosine = cos_lookup(rotation);
int32_t sine = sin_lookup(rotation);
GPoint result;
result.x = (int32_t)orig->x * cosine / TRIG_MAX_RATIO - (int32_t)orig->y * sine / TRIG_MAX_RATIO + offset->x;
result.y = (int32_t)orig->y * cosine / TRIG_MAX_RATIO + (int32_t)orig->x * sine / TRIG_MAX_RATIO + offset->y;
return result;
}
static void sort16(int16_t *values, size_t length) {
for (unsigned int i = 0; i < length; i++) {
for (unsigned int j = i+1; j < length; j++) {
if (values[i] > values[j]) {
swap16(&values[i], &values[j]);
}
}
}
}
static void swapIntersections(Intersection *a, Intersection *b) {
Intersection t = *a;
*a = *b;
*b = t;
}
static void sortIntersections(Intersection *values, size_t length) {
for (unsigned int i = 0; i < length; i++) {
for (unsigned int j = i+1; j < length; j++) {
if (values[i].x.raw_value > values[j].x.raw_value) {
swapIntersections(&values[i], &values[j]);
}
}
}
}
static inline bool prv_is_in_range(int16_t min_a, int16_t max_a, int16_t min_b, int16_t max_b) {
return (max_a >= min_b) && (min_a <= max_b);
}
static void prv_gpath_draw_filled_cb(GContext *ctx, int16_t y,
Fixed_S16_3 x_range_begin, Fixed_S16_3 x_range_end,
Fixed_S16_3 delta_begin, Fixed_S16_3 delta_end,
void *user_data) {
#if PBL_COLOR
// We know that correct delta is always positive, and treat that as an input from
// antialiased function, otherwise its treated as non-AA
if (delta_begin.raw_value >= 0 || delta_end.raw_value >= 0) {
x_range_begin.integer++;
x_range_end.integer--;
graphics_private_draw_horizontal_line_delta_aa(
ctx, y, x_range_begin, x_range_end, delta_begin, delta_end);
return;
}
#endif
graphics_fill_rect(
ctx, &(GRect) { { x_range_begin.integer + 1, y },
{ x_range_end.integer - x_range_begin.integer - 1, 1 } });
}
void gpath_draw_filled(GContext* ctx, GPath* path) {
#if PBL_COLOR
// This algorithm makes sense only in 8bit mode...
if (ctx->draw_state.antialiased) {
prv_fill_path_with_cb_aa(ctx, path, prv_gpath_draw_filled_cb, NULL);
return;
}
#endif
gpath_draw_filled_with_cb(ctx, path, prv_gpath_draw_filled_cb, NULL);
}
void gpath_draw_outline(GContext* ctx, GPath* path) {
gpath_draw_stroke(ctx, path, false);
}
void gpath_draw_outline_open(GContext* ctx, GPath* path) {
gpath_draw_stroke(ctx, path, true);
}
void gpath_draw_stroke(GContext* ctx, GPath* path, bool open) {
if (!path || path->num_points < 2) {
return;
}
// for each line segment (do not draw line returning to the first point if open is true)
for (uint32_t i = 0; i < (open ? (path->num_points - 1) : path->num_points); ++i) {
int i2 = (i + 1) % path->num_points;
GPoint rot_start = rotate_offset_point(&path->points[i], path->rotation, &path->offset);
GPoint rot_end = rotate_offset_point(&path->points[i2], path->rotation, &path->offset);
graphics_draw_line(ctx, rot_start, rot_end);
}
}
void gpath_rotate_to(GPath *path, int32_t angle) {
if (!path) {
return;
}
path->rotation = angle % TRIG_MAX_ANGLE;
}
void gpath_move_to(GPath *path, GPoint point) {
if (!path) {
return;
}
path->offset = point;
}
void gpath_move(GPath *path, GPoint delta) {
if (!path) {
return;
}
path->offset.x += delta.x;
path->offset.y += delta.y;
}
GRect gpath_outer_rect(GPath *path) {
if (!path) {
return GRect(0, 0, 0, 0);
}
int16_t max_x = INT16_MIN;
int16_t min_x = INT16_MAX;
int16_t max_y = INT16_MIN;
int16_t min_y = INT16_MAX;
for (uint32_t i = 0; i < path->num_points; ++i) {
if (path->points[i].x > max_x) {
max_x = path->points[i].x;
}
if (path->points[i].x < min_x) {
min_x = path->points[i].x;
}
if (path->points[i].y > max_y) {
max_y = path->points[i].y;
}
if (path->points[i].y < min_y) {
min_y = path->points[i].y;
}
}
return GRect(min_x, min_y, (max_x - min_x), (max_y - min_y));
}
#if PBL_COLOR
void prv_fill_path_with_cb_aa(GContext *ctx, GPath *path, GPathDrawFilledCallback cb,
void *user_data) {
/*
* Filling gpaths with antialiasing for integral-coordinates based paths:
*
* Custom linescanner using simple mathematic trick to determine anti-aliased edges
* 1. Rotate all points in path
* 2. Progress line-by-line finding intersections with paths
* 2.1 Calculate delta (angle) of the intersecting lines
* 2.2 Sort intersections
* 2.3 Draw lines between intersections
*
* This algorithm relies on few tricks:
* - For intersections with delta less than 1 (angle is less than 45°) we will use exact
* position of the intersection and fill edge pixel based on that information
* - For intersections with delta bigger than 1 (angle is bigger than 45°) we will use delta to
* draw gradient line responding to the angle
* + If gradient is bigger than distance from the start/end of the intersecting line
* we will adjust the delta to match starting/ending point and avoid nasty
* gradients diving in/out the path
* + Gradients too close to clipping rect will be properly cut off
*/
// Protect against apps calling with no points to draw (Upright watchface)
if (!path || path->num_points < 2) {
return;
}
GPointPrecise* rot_points = applib_malloc(path->num_points * sizeof(GPointPrecise));
if (!rot_points) {
return;
}
int min_x, max_x, min_y, max_y;
GPointPrecise rot_start, rot_end;
bool found_start_direction = false;
bool start_is_down = false;
Intersection *intersections_up = NULL;
Intersection *intersections_down = NULL;
rot_points[0] = rot_end = GPointPreciseFromGPoint(
rotate_offset_point(&path->points[0], path->rotation, &path->offset));
min_x = max_x = rot_points[0].x.integer;
min_y = max_y = rot_points[0].y.integer;
// begin finding the last path segment's direction going backwards through the path
// we must go backwards because we find intersections going forwards
for (int i = path->num_points - 1; i > 0; --i) {
rot_points[i] = rot_start = GPointPreciseFromGPoint(
rotate_offset_point(&path->points[i], path->rotation, &path->offset));
if (min_x > rot_points[i].x.integer) { min_x = rot_points[i].x.integer; }
if (max_x < rot_points[i].x.integer) { max_x = rot_points[i].x.integer; }
if (min_y > rot_points[i].y.integer) { min_y = rot_points[i].y.integer; }
if (max_y < rot_points[i].y.integer) { max_y = rot_points[i].y.integer; }
if (found_start_direction) {
continue;
}
// use the first non-horizontal path segment's direction as the start direction
if (rot_end.y.integer != rot_start.y.integer) {
start_is_down = rot_end.y.integer > rot_start.y.integer;
found_start_direction = true;
}
rot_end = rot_start;
}
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 = ctx->draw_state.clip_box.size.w + clip_min_x;
if (!prv_is_in_range(min_x, max_x, clip_min_x, clip_max_x)) {
goto cleanup;
}
// x-intersections of path segments whose direction is up
intersections_up = applib_zalloc(path->num_points * sizeof(Intersection));
// x-intersections of path segments whose direction is down
intersections_down = applib_zalloc(path->num_points * sizeof(Intersection));
// If either malloc failed, log message and cleanup
if (!intersections_up || !intersections_down) {
APP_LOG(APP_LOG_LEVEL_ERROR, GPATH_ERROR);
goto cleanup;
}
int intersection_up_count;
int intersection_down_count;
// convert clip coordinates to drawing coordinates
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 = ctx->draw_state.clip_box.size.h + clip_min_y;
min_y = MAX(min_y, clip_min_y);
max_y = MIN(max_y, clip_max_y);
// filling color hack
GColor tmp = ctx->draw_state.stroke_color;
ctx->draw_state.stroke_color = ctx->draw_state.fill_color;
// find all of the horizontal intersections and draw them
for (int16_t i = min_y; i <= max_y; ++i) {
// initialize with 0 intersections
intersection_down_count = 0;
intersection_up_count = 0;
// horizontal path segments don't have a direction and depend
// upon the last path segment's direction
// keep track of the last path direction for horizontal path segments to use
bool last_is_down = start_is_down;
rot_end = rot_points[0];
// find the intersections
for (uint32_t j = 0; j < path->num_points; ++j) {
rot_start = rot_points[j];
if (j + 1 < path->num_points) {
rot_end = rot_points[j + 1];
} else {
// wrap to the first point
rot_end = rot_points[0];
}
// if the line is on/crosses this height
if ((rot_start.y.integer - i) * (rot_end.y.integer - i) <= 0) {
bool is_down = rot_end.y.integer != rot_start.y.integer ?
rot_end.y.integer > rot_start.y.integer : last_is_down;
// don't count end points in the same direction to avoid double intersections
if (!(rot_start.y.integer == i && last_is_down == is_down)) {
// linear interpolation of the line intersection
int16_t delta_x = rot_end.x.raw_value - rot_start.x.raw_value;
int16_t delta_y = rot_end.y.raw_value - rot_start.y.raw_value;
Fixed_S16_3 x = (Fixed_S16_3){.raw_value = rot_start.x.raw_value + delta_x
* (i * FIXED_S16_3_ONE.raw_value - rot_start.y.raw_value) / delta_y};
Fixed_S16_3 delta = (Fixed_S16_3){.raw_value = ABS(delta_x / delta_y) *
FIXED_S16_3_ONE.raw_value};
if (delta.integer > 1) {
// this is where we try to fix edges diving in and out of paths
int16_t min_x = rot_end.x.raw_value < rot_start.x.raw_value ?
rot_end.x.raw_value : rot_start.x.raw_value;
int16_t max_x = rot_end.x.raw_value > rot_start.x.raw_value ?
rot_end.x.raw_value : rot_start.x.raw_value;
if (x.raw_value - (delta.raw_value / 2) < min_x) {
delta.raw_value = (x.raw_value - min_x) * 2;
}
if (x.raw_value + (delta.raw_value / 2) > max_x) {
delta.raw_value = (max_x - x.raw_value) * 2;
}
}
if (is_down) {
intersections_down[intersection_down_count].x.raw_value = x.raw_value;
intersections_down[intersection_down_count].delta = delta;
intersection_down_count++;
} else {
intersections_up[intersection_up_count].x.raw_value = x.raw_value;
intersections_up[intersection_up_count].delta = delta;
intersection_up_count++;
}
}
last_is_down = is_down;
}
}
// sort the intersections
sortIntersections(intersections_up, intersection_up_count);
sortIntersections(intersections_down, intersection_down_count);
// draw the line segments
for (int j = 0; j < MIN(intersection_up_count, intersection_down_count); j++) {
Intersection x_a = intersections_up[j];
Intersection x_b = intersections_down[j];
if (x_a.x.integer != x_b.x.integer) {
if (x_a.x.integer > x_b.x.integer) {
swapIntersections(&x_a, &x_b);
}
// this is done by callback now...
// x_a.x.integer++;
// x_b.x.integer--;
cb(ctx, i, x_a.x, x_b.x, x_a.delta, x_b.delta, user_data);
}
}
}
// restore original stroke color
ctx->draw_state.stroke_color = tmp;
cleanup:
applib_free(rot_points);
applib_free(intersections_up);
applib_free(intersections_down);
}
#endif // PBL_COLOR
void gpath_draw_filled_with_cb(GContext *ctx, GPath *path, GPathDrawFilledCallback cb,
void *user_data) {
//Protect against apps calling with no points to draw (Upright watchface)
if (!path || path->num_points < 2) {
return;
}
GPoint* rot_points = applib_malloc(path->num_points * sizeof(GPoint));
if (!rot_points) {
APP_LOG(APP_LOG_LEVEL_ERROR, GPATH_ERROR);
return;
}
int min_x, max_x, min_y, max_y;
GPoint rot_start, rot_end;
bool found_start_direction = false;
bool start_is_down = false;
int16_t *intersections_up = NULL;
int16_t *intersections_down = NULL;
rot_points[0] = rot_end = rotate_offset_point(&path->points[0], path->rotation, &path->offset);
min_x = max_x = rot_points[0].x;
min_y = max_y = rot_points[0].y;
// begin finding the last path segment's direction going backwards through the path
// we must go backwards because we find intersections going forwards
for (int i = path->num_points - 1; i > 0; --i) {
rot_points[i] = rot_start = rotate_offset_point(&path->points[i], path->rotation, &path->offset);
if (min_x > rot_points[i].x) { min_x = rot_points[i].x; }
if (max_x < rot_points[i].x) { max_x = rot_points[i].x; }
if (min_y > rot_points[i].y) { min_y = rot_points[i].y; }
if (max_y < rot_points[i].y) { max_y = rot_points[i].y; }
if (found_start_direction) {
continue;
}
// use the first non-horizontal path segment's direction as the start direction
if (rot_end.y != rot_start.y) {
start_is_down = rot_end.y > rot_start.y;
found_start_direction = true;
}
rot_end = rot_start;
}
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 = ctx->draw_state.clip_box.size.w + clip_min_x;
if (!prv_is_in_range(min_x, max_x, clip_min_x, clip_max_x)) {
goto cleanup;
}
// x-intersections of path segments whose direction is up
intersections_up = applib_zalloc(path->num_points * sizeof(int16_t));
// x-intersections of path segments whose direction is down
intersections_down = applib_zalloc(path->num_points * sizeof(int16_t));
// If either malloc failed, log message and cleanup
if (!intersections_up || !intersections_down) {
APP_LOG(APP_LOG_LEVEL_ERROR, GPATH_ERROR);
goto cleanup;
}
int intersection_up_count;
int intersection_down_count;
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 = ctx->draw_state.clip_box.size.h + clip_min_y;
min_y = MAX(min_y, clip_min_y);
max_y = MIN(max_y, clip_max_y);
// find all of the horizontal intersections and draw them
for (int16_t i = min_y; i <= max_y; ++i) {
// initialize with 0 intersections
intersection_down_count = 0;
intersection_up_count = 0;
// horizontal path segments don't have a direction and depend upon the last path segment's direction
// keep track of the last path direction for horizontal path segments to use
bool last_is_down = start_is_down;
rot_end = rot_points[0];
// find the intersections
for (uint32_t j = 0; j < path->num_points; ++j) {
rot_start = rot_points[j];
if (j + 1 < path->num_points) {
rot_end = rot_points[j + 1];
} else {
// wrap to the first point
rot_end = rot_points[0];
}
// if the line is on/crosses this height
if ((rot_start.y - i) * (rot_end.y - i) <= 0) {
bool is_down = rot_end.y != rot_start.y ? rot_end.y > rot_start.y : last_is_down;
// don't count end points in the same direction to avoid double intersections
if (!(rot_start.y == i && last_is_down == is_down)) {
// linear interpolation of the line intersection
int16_t x = rot_start.x + (rot_end.x - rot_start.x) * (i - rot_start.y) / (rot_end.y - rot_start.y);
if (is_down) {
intersections_down[intersection_down_count] = x;
intersection_down_count++;
} else {
intersections_up[intersection_up_count] = x;
intersection_up_count++;
}
}
last_is_down = is_down;
}
}
// sort the intersections
sort16(intersections_up, intersection_up_count);
sort16(intersections_down, intersection_down_count);
// draw the line segments
for (int j = 0; j < MIN(intersection_up_count, intersection_down_count); j++) {
int16_t x_a = intersections_up[j];
int16_t x_b = intersections_down[j];
if (x_a != x_b) {
if (x_a > x_b) {
swap16(&x_a, &x_b);
}
cb(ctx, i, (Fixed_S16_3){.integer = x_a}, (Fixed_S16_3){.integer = x_b},
(Fixed_S16_3){.integer = -1}, (Fixed_S16_3){.integer = -1}, user_data);
}
}
}
cleanup:
applib_free(rot_points);
applib_free(intersections_up);
applib_free(intersections_down);
}
void gpath_fill_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points) {
if (!points) {
return;
}
// Convert precise points to normal points and draw filled path with converted points
// (no real support for filled paths with GPointPrecise, yet)
GPoint *imprecise_points = applib_malloc(sizeof(GPoint) * num_points);
if (!imprecise_points) {
APP_LOG(APP_LOG_LEVEL_ERROR, GPATH_ERROR);
return;
}
for (size_t i = 0; i < num_points; i++) {
imprecise_points[i] = GPointFromGPointPrecise(points[i]);
}
GPath path = {
.num_points = (uint32_t)num_points,
.points = imprecise_points
};
gpath_draw_filled(ctx, &path);
applib_free(imprecise_points);
}
void gpath_draw_outline_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points,
bool open) {
if (!points) {
return;
}
// draw precise path (no real support currently for paths with GPointPrecise)
for (uint16_t i = 0; i < (open ? (num_points - 1) : num_points); ++i) {
size_t i2 = (i + 1) % num_points;
graphics_line_draw_precise_stroked(ctx, points[i], points[i2]);
}
}

View file

@ -0,0 +1,204 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
//! @addtogroup Graphics
//! @{
//! @addtogroup PathDrawing Drawing Paths
//! \brief Functions to draw polygons into a graphics context
//!
//! Code example:
//! \code{.c}
//! static GPath *s_my_path_ptr = NULL;
//!
//! static const GPathInfo BOLT_PATH_INFO = {
//! .num_points = 6,
//! .points = (GPoint []) {{21, 0}, {14, 26}, {28, 26}, {7, 60}, {14, 34}, {0, 34}}
//! };
//!
//! // .update_proc of my_layer:
//! void my_layer_update_proc(Layer *my_layer, GContext* ctx) {
//! // Fill the path:
//! graphics_context_set_fill_color(ctx, GColorWhite);
//! gpath_draw_filled(ctx, s_my_path_ptr);
//! // Stroke the path:
//! graphics_context_set_stroke_color(ctx, GColorBlack);
//! gpath_draw_outline(ctx, s_my_path_ptr);
//! }
//!
//! void setup_my_path(void) {
//! s_my_path_ptr = gpath_create(&BOLT_PATH_INFO);
//! // Rotate 15 degrees:
//! gpath_rotate_to(s_my_path_ptr, TRIG_MAX_ANGLE / 360 * 15);
//! // Translate by (5, 5):
//! gpath_move_to(s_my_path_ptr, GPoint(5, 5));
//! }
//!
//! // For brevity, the setup of my_layer is not written out...
//! \endcode
//! @{
//! Data structure describing a naked path
//! @note Note that this data structure only refers to an array of points;
//! the points are not stored inside this data structure itself.
//! In most cases, one cannot use a stack-allocated array of GPoints. Instead
//! one often needs to provide longer-lived (static or "global") storage for the points.
typedef struct GPathInfo {
//! The number of points in the `points` array
uint32_t num_points;
//! Pointer to an array of points.
GPoint *points;
} GPathInfo;
//! Data structure describing a path, plus its rotation and translation.
//! @note See the remark with \ref GPathInfo
typedef struct GPath {
//! The number of points in the `points` array
uint32_t num_points;
//! Pointer to an array of points.
GPoint *points;
//! The rotation that will be used when drawing the path with
//! \ref gpath_draw_filled() or \ref gpath_draw_outline()
int32_t rotation;
//! The translation that will to be used when drawing the path with
//! \ref gpath_draw_filled() or \ref gpath_draw_outline()
GPoint offset;
} GPath;
//! @internal
//! Initializes a GPath based on a series of points described by a GPathInfo.
void gpath_init(GPath *path, const GPathInfo *init);
//! Creates a new GPath on the heap based on a series of points described by a GPathInfo.
//!
//! Values after initialization:
//! * `num_points` and `points` pointer: copied from the GPathInfo.
//! * `rotation`: 0
//! * `offset`: (0, 0)
//! @return A pointer to the GPath. `NULL` if the GPath could not
//! be created
GPath* gpath_create(const GPathInfo *init);
//! Free a dynamically allocated gpath created with \ref gpath_create()
void gpath_destroy(GPath* gpath);
//! Draws the fill of a path into a graphics context, using the current fill color,
//! relative to the drawing area as set up by the layering system.
//! @param ctx The graphics context to draw into
//! @param path The path to fill
//! @see \ref graphics_context_set_fill_color()
void gpath_draw_filled(GContext* ctx, GPath *path);
//! Draws the outline of a path into a graphics context, using the current stroke color and
//! width, relative to the drawing area as set up by the layering system. The first and last points
//! in the path do have a line between them.
//! @param ctx The graphics context to draw into
//! @param path The path to draw
//! @see \ref graphics_context_set_stroke_color()
//! @see \ref gpath_draw_outline_open()
void gpath_draw_outline(GContext* ctx, GPath *path);
//! Draws an open outline of a path into a graphics context, using the current stroke color and
//! width, relative to the drawing area as set up by the layering system. The first and last points
//! in the path do not have a line between them.
//! @param ctx The graphics context to draw into
//! @param path The path to draw
//! @see \ref graphics_context_set_stroke_color()
//! @see \ref gpath_draw_outline()
void gpath_draw_outline_open(GContext* ctx, GPath* path);
//! @internal
//! Draws a stroke following a path into a graphics context, using the current stroke color and
//! width, relative to the drawing area as set up by the layering system.
//! @param ctx The graphics context to draw into
//! @param path The path to draw
//! @param open true if path must be left open (not closed between first and last points)
//! @see \ref graphics_context_set_stroke_color()
void gpath_draw_stroke(GContext* ctx, GPath *path, bool open);
//! Sets the absolute rotation of the path.
//! The current rotation will be replaced by the specified angle.
//! @param path The path onto which to set the rotation
//! @param angle The absolute angle of the rotation. The angle is represented in the same way
//! that is used with \ref sin_lookup(). See \ref TRIG_MAX_ANGLE for more information.
//! @note Setting a rotation does not affect the points in the path directly.
//! The rotation is applied on-the-fly during drawing, either using \ref gpath_draw_filled() or
//! \ref gpath_draw_outline().
void gpath_rotate_to(GPath *path, int32_t angle);
//! Applies a relative rotation to the path.
//! The angle will be added to the current rotation of the path.
//! @param path The path onto which to apply the rotation
//! @param delta_angle The relative angle of the rotation. The angle is represented in the same way
//! that is used with \ref sin_lookup(). See \ref TRIG_MAX_ANGLE for more information.
//! @note Applying a rotation does not affect the points in the path directly.
//! The rotation is applied on-the-fly during drawing, either using \ref gpath_draw_filled() or
//! \ref gpath_draw_outline().
void gpath_rotate(GPath *path, int32_t delta_angle);
//! Sets the absolute offset of the path.
//! The current translation will be replaced by the specified offset.
//! @param path The path onto which to set the translation
//! @param point The point which is used as the vector for the translation.
//! @note Setting a translation does not affect the points in the path directly.
//! The translation is applied on-the-fly during drawing, either using \ref gpath_draw_filled() or
//! \ref gpath_draw_outline().
void gpath_move_to(GPath *path, GPoint point);
//! Applies a relative offset to the path.
//! The offset will be added to the current translation of the path.
//! @param path The path onto which to apply the translation
//! @param delta The point which is used as the vector for the translation.
//! @note Applying a translation does not affect the points in the path directly.
//! The translation is applied on-the-fly during drawing, either using \ref gpath_draw_filled() or
//! \ref gpath_draw_outline().
void gpath_move(GPath *path, GPoint delta);
//! Calculates the outer rectangle of the path's points,
//! ignoring the offset and rotation that might be set.
GRect gpath_outer_rect(GPath *path);
//! @internal
//! Drawing function callback
//! @param ctx GContext of drawing
//! @param y integral Y coordinate of drawn line
//! @param x_range_begin precise X coordinate of beginning of the line
//! @param x_range_end precise X coordinate of ending of the line
//! @param delta_begin Delta of the line crossing x_range_begin - negative if no AA
//! @param delta_end Delta of the line crossing x_range_end - negative if no AA
//! @param user_data User data for extra data the callback may require
typedef void (*GPathDrawFilledCallback)(
GContext *ctx, int16_t y, Fixed_S16_3 x_range_begin, Fixed_S16_3 x_range_end,
Fixed_S16_3 delta_begin, Fixed_S16_3 delta_end, void *user_data);
//! @internal
//! Allows for customized drawing of a GContext's drawing_box with a GPath defining "inside" and
//! "outside" regions. Nothing is drawn by this method, all drawing should be done by the supplied
//! callback.
void gpath_draw_filled_with_cb(GContext *ctx, GPath *path, GPathDrawFilledCallback cb,
void *user_data);
//! @internal
void gpath_fill_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points);
//! @internal
void gpath_draw_outline_precise_internal(GContext *ctx, GPointPrecise *points, size_t num_points,
bool open);
//! @} // end addtogroup PathDrawing
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,167 @@
/*
* 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 "gpath_builder.h"
#include "gpath.h"
#include "applib/applib_malloc.auto.h"
#include "util/trig.h"
#include <string.h>
const int fixedpoint_base = 16;
// Angle below which we're not going to process with recursion
int32_t max_angle_tolerance = (TRIG_MAX_ANGLE / 360) * 10;
bool recursive_bezier_fixed(GPathBuilder *builder,
int32_t x1, int32_t y1,
int32_t x2, int32_t y2,
int32_t x3, int32_t y3,
int32_t x4, int32_t y4) {
// Calculate all the mid-points of the line segments
int32_t x12 = (x1 + x2) / 2;
int32_t y12 = (y1 + y2) / 2;
int32_t x23 = (x2 + x3) / 2;
int32_t y23 = (y2 + y3) / 2;
int32_t x34 = (x3 + x4) / 2;
int32_t y34 = (y3 + y4) / 2;
int32_t x123 = (x12 + x23) / 2;
int32_t y123 = (y12 + y23) / 2;
int32_t x234 = (x23 + x34) / 2;
int32_t y234 = (y23 + y34) / 2;
int32_t x1234 = (x123 + x234) / 2;
int32_t y1234 = (y123 + y234) / 2;
// Angle Condition
int32_t a23 = atan2_lookup((int16_t)((y3 - y2) / fixedpoint_base),
(int16_t)((x3 - x2) / fixedpoint_base));
int32_t da1 = abs(a23 - atan2_lookup((int16_t)((y2 - y1) / fixedpoint_base),
(int16_t)((x2 - x1) / fixedpoint_base)));
int32_t da2 = abs(atan2_lookup((int16_t)((y4 - y3) / fixedpoint_base),
(int16_t)((x4 - x3) / fixedpoint_base)) - a23);
if (da1 >= TRIG_MAX_ANGLE) {
da1 = TRIG_MAX_ANGLE - da1;
}
if (da2 >= TRIG_MAX_ANGLE) {
da2 = TRIG_MAX_ANGLE - da2;
}
if (da1 + da2 < max_angle_tolerance) {
// Finally we can stop the recursion
return gpath_builder_line_to_point(builder, GPoint(x1234 / fixedpoint_base,
y1234 / fixedpoint_base));
}
// Continue subdivision if points are being added successfully
if (recursive_bezier_fixed(builder, x1, y1, x12, y12, x123, y123, x1234, y1234)
&& recursive_bezier_fixed(builder, x1234, y1234, x234, y234, x34, y34, x4, y4)) {
return true;
}
return false;
}
bool bezier_fixed(GPathBuilder *builder, GPoint p1, GPoint p2, GPoint p3, GPoint p4) {
// Translate points to fixedpoint realms
int32_t x1 = p1.x * fixedpoint_base;
int32_t x2 = p2.x * fixedpoint_base;
int32_t x3 = p3.x * fixedpoint_base;
int32_t x4 = p4.x * fixedpoint_base;
int32_t y1 = p1.y * fixedpoint_base;
int32_t y2 = p2.y * fixedpoint_base;
int32_t y3 = p3.y * fixedpoint_base;
int32_t y4 = p4.y * fixedpoint_base;
if (recursive_bezier_fixed(builder, x1, y1, x2, y2, x3, y3, x4, y4)) {
return gpath_builder_line_to_point(builder, p4);
}
return false;
}
GPathBuilder *gpath_builder_create(uint32_t max_points) {
// Allocate enough memory to store all the points - points are stored contiguously with the
// GPathBuilder structure
const size_t required_size = sizeof(GPathBuilder) + max_points * sizeof(GPoint);
GPathBuilder *result = applib_malloc(required_size);
if (!result) {
return NULL;
}
memset(result, 0, required_size);
result->max_points = max_points;
return result;
}
void gpath_builder_destroy(GPathBuilder *builder) {
applib_free(builder);
}
GPath *gpath_builder_create_path(GPathBuilder *builder) {
if (builder->num_points <= 1) {
return NULL;
}
uint32_t num_points = builder->num_points;
// handle case where last point == first point => remove last point
while (num_points > 1
&& gpoint_equal(&builder->points[0], &builder->points[num_points])) {
num_points--;
}
// Allocate enough memory for both the GPath structure as well as the array of GPoints.
// Both will be contiguous in memory.
const size_t size_of_points = num_points * sizeof(GPoint);
GPath *result = applib_malloc(sizeof(GPath) + size_of_points);
if (!result) {
return NULL;
}
memset(result, 0, sizeof(GPath));
result->num_points = num_points;
// Set the points pointer within the GPath structure to point just after the GPath structure
// since that is where memory has been allocated for the array.
result->points = (GPoint*)(result + 1);
memcpy(result->points, builder->points, size_of_points);
return result;
}
bool gpath_builder_move_to_point(GPathBuilder *builder, GPoint to_point) {
if (builder->num_points != 0) {
return false;
}
return gpath_builder_line_to_point(builder, to_point);
}
bool gpath_builder_line_to_point(GPathBuilder *builder, GPoint to_point) {
if (builder->num_points >= builder->max_points - 1) {
return false;
}
builder->points[builder->num_points++] = to_point;
return true;
}
bool gpath_builder_curve_to_point(GPathBuilder *builder, GPoint to_point,
GPoint control_point_1, GPoint control_point_2) {
GPoint from_point = builder->points[builder->num_points-1];
return bezier_fixed(builder, from_point, control_point_1, control_point_2, to_point);
}

View file

@ -0,0 +1,112 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
#include "gpath.h"
//! @addtogroup Graphics
//! @{
//! @addtogroup PathBuilding Building Paths
//! \brief Functions to build GPath objects without using GPathInfo
//!
//! Code example:
//! \code{.c}
//! #define MAX_POINTS 256
//! static GPath *s_path;
//!
//! // .update_proc of my_layer:
//! void my_layer_update_proc(Layer *my_layer, GContext* ctx) {
//! // Fill the path:
//! graphics_context_set_fill_color(ctx, GColorWhite);
//! gpath_draw_filled(ctx, s_path);
//! // Stroke the path:
//! graphics_context_set_stroke_color(ctx, GColorBlack);
//! gpath_draw_outline(ctx, s_path);
//! }
//!
//! void build_my_path(void) {
//! // Specify how many points builder can create
//! GPathBuilder *builder = gpath_builder_create(MAX_POINTS);
//! // Use gpath builder API to create path
//! gpath_builder_move_to_point(builder, GPoint(0, -60));
//! gpath_builder_curve_to_point(builder, GPoint(60, 0), GPoint(35, -60), GPoint(60, -35));
//! gpath_builder_curve_to_point(builder, GPoint(0, 60), GPoint(60, 35), GPoint(35, 60));
//! gpath_builder_curve_to_point(builder, GPoint(0, 0), GPoint(-50, 60), GPoint(-50, 0));
//! gpath_builder_curve_to_point(builder, GPoint(0, -60), GPoint(50, 0), GPoint(50, -60));
//! // Convert the result to GPath object for drawing
//! s_path = gpath_builder_create_path(builder);
//! // Destroy the builder
//! gpath_builder_destroy(builder);
//! }
//! \endcode
//! @{
//! Data structure used by gpath builder
//! @note This structure is being filled by gpath builder
typedef struct {
//! Maximum number of points that builder can create and size of `points` array
uint32_t max_points;
//! The number of points in `points` array
uint32_t num_points;
//! Array containing points
GPoint points[];
} GPathBuilder;
//! Creates new GPathBuilder object on the heap sized accordingly to maximum number
//! of points given
//!
//! @param max_points Size of the points buffer
//! @return A pointer to GPathBuilder. NULL if object couldnt be created
GPathBuilder *gpath_builder_create(uint32_t max_points);
//! Destroys GPathBuilder previously created with gpath_builder_create()
void gpath_builder_destroy(GPathBuilder *builder);
//! Sets starting point for GPath
//! @param builder GPathBuilder object to manipulate on
//! @param to_point starting point for the GPath
//! @return True if point was moved successfully False if there was no space in
//! GPathBuilder struct or there was segment added already
bool gpath_builder_move_to_point(GPathBuilder *builder, GPoint to_point);
//! Makes straight line from current point to point given and makes it new current point
//! @param builder GPathBuilder object to manipulate on
//! @param to_point ending point for the line
//! @return True if line was added successfully False if there was no space in GPathBuilder struct
bool gpath_builder_line_to_point(GPathBuilder *builder, GPoint to_point);
//! Makes bezier curve from current point to point given and makes it new current point,
//! quadratic bezier curve is created based on control points
//! @param builder GPathBuilder object to manipulate on
//! @param to_point ending point for bezier curve
//! @param control_point_1 control point for start of the bezier curve
//! @param control_point_2 control point for end of the bezier curve
//! @return True if curve was added successfully False if there was no space in GPathBuilder struct
bool gpath_builder_curve_to_point(GPathBuilder *builder, GPoint to_point,
GPoint control_point_1, GPoint control_point_2);
//! Creates a new GPath on the heap based on a data from GPathBuilder
//!
//! Values after initialization:
//! * `num_points` and `points` pointer: copied from the GPathBuilder
//! * `rotation`: 0
//! * `offset`: (0, 0)
//! @return A pointer to the GPath. `NULL` if num_points less than 2 or not enough memory
GPath *gpath_builder_create_path(GPathBuilder *builder);
//! @} // end addtogroup PathBuilding
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,717 @@
/*
* 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.h"
#include "bitblt.h"
#include "bitblt_private.h"
#include "framebuffer.h"
#include "graphics_private.h"
#include "graphics_private_raw.h"
#include "gtransform.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "kernel/ui/kernel_ui.h"
#include "process_management/process_manager.h"
#include "process_state/app_state/app_state.h"
#include "system/passert.h"
#include "system/logging.h"
#include "util/bitset.h"
#include "util/graphics.h"
#include "util/math.h"
#include "util/reverse.h"
#include "util/trig.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#if !defined(__clang__)
#pragma GCC optimize ("O3")
#endif
void graphics_draw_pixel(GContext* ctx, GPoint point) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
point.x += ctx->draw_state.drawing_box.origin.x;
point.y += ctx->draw_state.drawing_box.origin.y;
graphics_private_set_pixel(ctx, point);
}
T_STATIC void prv_fill_rect_legacy2(GContext *ctx, GRect rect, uint16_t radius,
GCornerMask corner_mask, GColor fill_color) {
if (gcolor_is_transparent(fill_color)) {
fill_color = GColorWhite;
}
// as this function will only be called with radius 0 to or
// to support the legacy2 behavior (where the radius is clamped to 8) it's safe to assume 8px here
PBL_ASSERTN(radius <= 8);
GBitmap* bitmap = graphics_context_get_bitmap(ctx);
// translate to absolute bitmap coordinates:
rect.origin.x += ctx->draw_state.drawing_box.origin.x;
rect.origin.y += ctx->draw_state.drawing_box.origin.y;
// clip it to avoid drawing outside of the bitmap memory:
GRect clipped_rect = rect;
grect_standardize(&clipped_rect);
grect_clip(&clipped_rect, &bitmap->bounds);
grect_clip(&clipped_rect, &ctx->draw_state.clip_box);
if (grect_is_empty(&clipped_rect)) {
return;
}
// All the row insets are packed into an uint32, taking 4 bits per inset (hence the 8px radius limit):
static const uint32_t round_top_corner_lookup[] = {
0x0, 0x01, 0x01, 0x12, 0x113, 0x123, 0x1234, 0x11235, 0x112346,
};
static const uint32_t round_bottom_corner_lookup[] = {
0x0, 0x01, 0x10, 0x210, 0x3110, 0x32100, 0x432100, 0x5321100, 0x64321100,
};
// Set up the insets for doing the top corners.
uint32_t corner_insets_left = (corner_mask & GCornerTopLeft) ? round_top_corner_lookup[radius] : 0;
uint32_t corner_insets_right = (corner_mask & GCornerTopRight) ? round_top_corner_lookup[radius] : 0;
const unsigned int top_cropped_rows_count = clipped_rect.origin.y - rect.origin.y;
const int32_t left_cropped_columns_count = MAX(0, clipped_rect.origin.x - rect.origin.x);
const int32_t right_cropped_columns_count = MAX(0, rect.size.w - clipped_rect.size.w -
left_cropped_columns_count);
if (top_cropped_rows_count) {
// Skip over rows for each one that's cropped off the top.
corner_insets_left >>= 4 * MIN(top_cropped_rows_count, 8);
corner_insets_right >>= 4 * MIN(top_cropped_rows_count, 8);
}
// Mark the destination dirty before clipped_rect is modified.
graphics_context_mark_dirty_rect(ctx, clipped_rect);
// bit-block fiddling:
const int16_t max_y = clipped_rect.origin.y + clipped_rect.size.h;
for (; clipped_rect.origin.y < max_y; ++clipped_rect.origin.y) {
if ((clipped_rect.origin.y == (rect.origin.y + rect.size.h) - radius) && (corner_mask & GCornersBottom)) {
if (corner_mask & GCornerBottomLeft) {
corner_insets_left = round_bottom_corner_lookup[radius];
}
if (corner_mask & GCornerBottomRight) {
corner_insets_right = round_bottom_corner_lookup[radius];
}
}
int32_t left_side = MAX((int32_t)(corner_insets_left & 0xf) - left_cropped_columns_count, 0);
int32_t right_side = MAX((int32_t)(corner_insets_right & 0xf) - right_cropped_columns_count, 0);
int32_t corner_insets = left_side + right_side;
int32_t width = corner_insets < clipped_rect.size.w ? (clipped_rect.size.w - corner_insets) : 0;
uint32_t x = clipped_rect.origin.x + left_side;
corner_insets_left >>= 4;
corner_insets_right >>= 4;
PBL_ASSERTN(clipped_rect.origin.y < bitmap->bounds.size.h);
PBL_ASSERTN(clipped_rect.origin.y >= 0);
const uint16_t y = clipped_rect.origin.y;
const uint16_t x_end = x + width;
graphics_private_draw_horizontal_line_integral(ctx, &ctx->dest_bitmap, y, x, x_end, fill_color);
}
}
//! Return the maximum rounded corner radius allowed for a given rectangle size
T_STATIC uint16_t prv_clamp_corner_radius(GSize size, GCornerMask corner_mask, uint16_t radius) {
if (corner_mask == GCornerNone) {
return 0;
}
int16_t min_size = MIN(size.w, size.h);
if (min_size >= 2 * radius) {
return radius;
} else {
return (min_size / 2);
}
}
typedef void (*FillCircleImplFunc)(GContext *, GPoint pt, uint16_t radius, GCornerMask mask);
//! generic fill_rect implementation to avoid code-duplication between aa and non-aa fill_rect
void prv_fill_rect_internal(GContext *ctx, const GRect *rect, uint16_t radius,
GCornerMask corner_mask, GColor fill_color, uint16_t alt_radius,
FillCircleImplFunc circle_func) {
// only draw if there is enough to cover the rounded edges - otherwise round down to largest
// radius that can be drawn
radius = prv_clamp_corner_radius(rect->size, corner_mask, radius);
if (radius <= alt_radius) {
prv_fill_rect_legacy2(ctx, *rect, radius, corner_mask, fill_color);
} else {
// These are used to optimize the rectangles that are drawn such that only three rectangles
// are drawn always
int16_t top_rect_origin_x = rect->origin.x;
int16_t top_rect_size_w = rect->size.w;
int16_t bottom_rect_origin_x = rect->origin.x;
int16_t bottom_rect_size_w = rect->size.w;
// Fill 3 rectangles and 4 quadrants
if (corner_mask & GCornerTopLeft) {
circle_func(ctx, GPoint(rect->origin.x + radius, rect->origin.y + radius),
radius, GCornerTopLeft);
top_rect_origin_x += radius;
top_rect_size_w -= radius;
}
if (corner_mask & GCornerBottomLeft) {
circle_func(ctx, GPoint(rect->origin.x + radius, rect->origin.y + rect->size.h - radius - 1),
radius, GCornerBottomLeft);
bottom_rect_origin_x += radius;
bottom_rect_size_w -= radius;
}
if (corner_mask & GCornerTopRight) {
circle_func(ctx, GPoint(rect->origin.x + rect->size.w - radius - 1, rect->origin.y + radius),
radius, GCornerTopRight);
top_rect_size_w -= radius;
}
if (corner_mask & GCornerBottomRight) {
circle_func(ctx, GPoint(rect->origin.x + rect->size.w - radius - 1,
rect->origin.y + rect->size.h - radius - 1),
radius, GCornerBottomRight);
bottom_rect_size_w -= radius;
}
// Top Rect
prv_fill_rect_legacy2(ctx, GRect(top_rect_origin_x, rect->origin.y, top_rect_size_w, radius),
0, GCornerNone, fill_color);
// Middle Rect
prv_fill_rect_legacy2(ctx, GRect(rect->origin.x, rect->origin.y + radius,
rect->size.w, rect->size.h - 2 * radius),
0, GCornerNone, fill_color);
// Bottom Rect
prv_fill_rect_legacy2(ctx, GRect(bottom_rect_origin_x, rect->origin.y + rect->size.h - radius,
bottom_rect_size_w, radius),
0, GCornerNone, fill_color);
}
}
T_STATIC void prv_fill_rect_non_aa(GContext* ctx, const GRect *rect, uint16_t radius,
GCornerMask corner_mask, GColor fill_color) {
// for radii <= 8 we can safely use the legacy2 behavior
const uint16_t alt_radius = 8;
FillCircleImplFunc circle_func = graphics_circle_quadrant_fill_non_aa;
prv_fill_rect_internal(ctx, rect, radius, corner_mask, fill_color, alt_radius, circle_func);
}
#if PBL_COLOR
T_STATIC void prv_fill_rect_aa(GContext* ctx, const GRect *rect, uint16_t radius,
GCornerMask corner_mask, GColor fill_color) {
FillCircleImplFunc circle_func = graphics_internal_circle_quadrant_fill_aa;
prv_fill_rect_internal(ctx, rect, radius, corner_mask, fill_color, 0, circle_func);
}
#endif // PBL_COLOR
void graphics_fill_round_rect(GContext* ctx, const GRect *rect, uint16_t radius,
GCornerMask corner_mask) {
PBL_ASSERTN(ctx);
if (!rect || ctx->lock) {
return;
}
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
// Antialiased (not suppported on 1-bit color)
prv_fill_rect_aa(ctx, rect, radius, corner_mask, ctx->draw_state.fill_color);
return;
}
#endif
prv_fill_rect_non_aa(ctx, rect, radius, corner_mask, ctx->draw_state.fill_color);
}
void graphics_fill_round_rect_by_value(GContext* ctx, GRect rect, uint16_t radius,
GCornerMask corner_mask) {
graphics_fill_round_rect(ctx, &rect, radius, corner_mask);
}
void graphics_fill_rect(GContext* ctx, const GRect *rect) {
graphics_fill_round_rect(ctx, rect, 0, GCornerNone);
}
T_STATIC void prv_draw_rect(GContext *ctx, const GRect *rect) {
GColor fill_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
graphics_fill_rect(ctx, &GRect(rect->origin.x, rect->origin.y, rect->size.w, 1)); // top
graphics_fill_rect(ctx, &GRect(rect->origin.x, rect->origin.y + rect->size.h - 1,
rect->size.w, 1)); // bottom
graphics_fill_rect(ctx, &GRect(rect->origin.x, rect->origin.y + 1, 1, rect->size.h - 2)); // left
graphics_fill_rect(ctx, &GRect(rect->origin.x + rect->size.w - 1,
rect->origin.y + 1, 1, rect->size.h - 2)); // right
ctx->draw_state.fill_color = fill_color;
}
#if PBL_COLOR
T_STATIC void prv_draw_rect_aa_stroked(GContext *ctx, const GRect *rect, uint8_t stroke_width) {
const GPoint tl = GPoint(rect->origin.x, rect->origin.y);
const GPoint tr = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y);
const GPoint bl = GPoint(rect->origin.x, rect->origin.y + rect->size.h - 1);
const GPoint br = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y + rect->size.h - 1);
graphics_line_draw_stroked_aa(ctx, tl, tr, stroke_width);
graphics_line_draw_stroked_aa(ctx, tl, bl, stroke_width);
graphics_line_draw_stroked_aa(ctx, tr, br, stroke_width);
graphics_line_draw_stroked_aa(ctx, bl, br, stroke_width);
}
#endif // PBL_COLOR
T_STATIC void prv_draw_rect_stroked(GContext *ctx, const GRect *rect, uint8_t stroke_width) {
const GPoint tl = GPoint(rect->origin.x, rect->origin.y);
const GPoint tr = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y);
const GPoint bl = GPoint(rect->origin.x, rect->origin.y + rect->size.h - 1);
const GPoint br = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y + rect->size.h - 1);
graphics_line_draw_stroked_non_aa(ctx, tl, tr, stroke_width);
graphics_line_draw_stroked_non_aa(ctx, tl, bl, stroke_width);
graphics_line_draw_stroked_non_aa(ctx, tr, br, stroke_width);
graphics_line_draw_stroked_non_aa(ctx, bl, br, stroke_width);
}
void graphics_draw_rect(GContext* ctx, const GRect *rect) {
PBL_ASSERTN(ctx);
if (!rect || ctx->lock) {
return;
}
if (ctx->draw_state.stroke_width <= 2) {
// Note: stroke width == 2 is rounded down to stroke width of 1
prv_draw_rect(ctx, rect);
return;
}
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
// Antialiased and Stroke Width > 2
prv_draw_rect_aa_stroked(ctx, rect, ctx->draw_state.stroke_width);
return;
}
#endif
// Non-Antialiased and Stroke Width > 2
// Note: stroke width must be odd and greater than 2
prv_draw_rect_stroked(ctx, rect, ctx->draw_state.stroke_width);
}
void graphics_draw_rect_by_value(GContext* ctx, GRect rect) {
graphics_draw_rect(ctx, &rect);
}
void graphics_draw_rect_precise(GContext* ctx, const GRectPrecise *rect) {
const Fixed_S16_3 right = grect_precise_get_max_x(rect);
const Fixed_S16_3 bottom = grect_precise_get_max_y(rect);
const GPointPrecise top_left = rect->origin;
const GPointPrecise top_right = {right, rect->origin.y};
const GPointPrecise bottom_right = {right, bottom};
const GPointPrecise bottom_left = {rect->origin.x, bottom};
graphics_line_draw_precise_stroked(ctx, top_left, top_right);
graphics_line_draw_precise_stroked(ctx, top_right, bottom_right);
graphics_line_draw_precise_stroked(ctx, bottom_right, bottom_left);
graphics_line_draw_precise_stroked(ctx, bottom_left, top_left);
}
// This takes care of all routines since it re-uses existing AA and SW functionality in draw line
// and draw circle
T_STATIC void prv_draw_round_rect(GContext* ctx, const GRect *rect, uint16_t radius) {
const GPoint origin = rect->origin;
const int16_t width = rect->size.w;
const int16_t height = rect->size.h;
// Subtract out twice the respective radius values to get the actual width and height of the
// rectangle lines
const int16_t width_actual = width - (2 * radius);
const int16_t height_actual = height - (2 * radius);
// Take into account the radius values to determine the eight points for each of the four lines
const GPoint top_l = GPoint(origin.x + radius, origin.y);
const GPoint top_r = GPoint(origin.x + radius + width_actual - 1, origin.y);
const GPoint bottom_l = GPoint(origin.x + radius, origin.y + height - 1);
const GPoint bottom_r = GPoint(origin.x + radius + width_actual - 1, origin.y + height - 1);
const GPoint left_t = GPoint(origin.x, origin.y + radius);
const GPoint left_b = GPoint(origin.x, origin.y + radius + height_actual - 1);
const GPoint right_t = GPoint(origin.x + width - 1, origin.y + radius);
const GPoint right_b = GPoint(origin.x + width - 1, origin.y + radius + height_actual - 1);
// Draw lines between each transformed corner point
graphics_draw_line(ctx, top_l, top_r); // top
graphics_draw_line(ctx, bottom_l, bottom_r); // bottom
graphics_draw_line(ctx, left_t, left_b); // left
graphics_draw_line(ctx, right_t, right_b); // right
// Draw quadrants
const GPoint tl = GPoint(origin.x + radius, origin.y + radius);
const GPoint tr = gpoint_add(tl, GPoint(width_actual - 1, 0));
const GPoint bl = gpoint_add(tl, GPoint(0, height_actual - 1));
const GPoint br = gpoint_add(tl, GPoint(width_actual - 1, height_actual - 1));
graphics_circle_quadrant_draw(ctx, tl, radius, GCornerTopLeft);
graphics_circle_quadrant_draw(ctx, bl, radius, GCornerBottomLeft);
graphics_circle_quadrant_draw(ctx, tr, radius, GCornerTopRight);
graphics_circle_quadrant_draw(ctx, br, radius, GCornerBottomRight);
}
#if PBL_COLOR
T_STATIC void prv_draw_round_rect_aa(GContext* ctx, const GRect *rect, uint16_t radius) {
// Assumes AA and stroke_width is set appropriately in ctx
prv_draw_round_rect(ctx, rect, radius);
}
T_STATIC void prv_draw_round_rect_aa_stroked(GContext* ctx, const GRect *rect,
uint16_t radius, uint8_t stroke_width) {
// Assumes AA and stroke_width is set appropriately in ctx
prv_draw_round_rect(ctx, rect, radius);
}
#endif // SCREEN_COLOR_DEPTH_BITS
T_STATIC void prv_draw_round_rect_stroked(GContext* ctx, const GRect *rect, uint16_t radius,
uint8_t stroke_width) {
// Assumes AA and stroke_width is set appropriately in ctx
prv_draw_round_rect(ctx, rect, radius);
}
static void prv_graphics_convert_8_bit_to_1_bit(const GBitmap *from, GBitmap *to) {
const GRect bounds = from->bounds;
uint8_t *to_buffer = (uint8_t *) to->addr;
const int y_start = bounds.origin.y;
const int y_end = y_start + bounds.size.h;
const int x_start = bounds.origin.x;
const int x_end = x_start + bounds.size.w;
for (int y = y_start; y < y_end; ++y) {
int to_idx_base = y * to->row_size_bytes;
uint8_t *line = to_buffer + to_idx_base;
for (int x = x_start; x < x_end; ++x) {
bitset8_clear(line, x);
}
}
}
void graphics_draw_round_rect(GContext* ctx, const GRect *rect, uint16_t radius) {
PBL_ASSERTN(ctx);
if (!rect || ctx->lock) {
return;
}
// only draw if there is enough to cover the rounded edges - otherwise round down to largest
// radius that can be drawn
radius = prv_clamp_corner_radius(rect->size, GCornersAll, radius);
if (radius == 0) {
graphics_draw_rect(ctx, rect);
} else {
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
if (ctx->draw_state.stroke_width > 1) {
// Antialiased and Stroke Width > 1
// Note: stroke width == 2 is rounded down to stroke width of 1
prv_draw_round_rect_aa_stroked(ctx, rect, radius, ctx->draw_state.stroke_width);
return;
} else {
// Antialiased and Stroke Width == 1 (not suppported on 1-bit color)
// Note: stroke width == 2 is rounded down to stroke width of 1
prv_draw_round_rect_aa(ctx, rect, radius);
return;
}
}
#endif
if (ctx->draw_state.stroke_width > 1) {
// Non-Antialiased and Stroke Width > 1
prv_draw_round_rect_stroked(ctx, rect, radius, ctx->draw_state.stroke_width);
} else {
// Non-Antialiased and Stroke Width == 1
prv_draw_round_rect(ctx, rect, radius);
}
}
}
void graphics_draw_round_rect_by_value(GContext* ctx, GRect rect, uint16_t radius) {
graphics_draw_round_rect(ctx, &rect, radius);
}
void graphics_context_init(GContext *context, FrameBuffer *framebuffer,
GContextInitializationMode init_mode) {
PBL_ASSERTN(context);
PBL_ASSERTN(framebuffer);
*context = (GContext) {
// For apps, this is run before the app has a chance to run, so there's no concern here of the
// app changing its framebuffer size.
.dest_bitmap = framebuffer_get_as_bitmap(framebuffer, &framebuffer->size),
.parent_framebuffer = framebuffer,
.parent_framebuffer_vertical_offset = 0,
.lock = false
};
// init the font cache
FontCache *font_cache = &context->font_cache;
memset(font_cache->cache_keys, 0, sizeof(font_cache->cache_keys));
memset(font_cache->cache_data, 0, sizeof(font_cache->cache_data));
keyed_circular_cache_init(&font_cache->line_cache, font_cache->cache_keys,
font_cache->cache_data, sizeof(LineCacheData), LINE_CACHE_SIZE);
graphics_context_set_default_drawing_state(context, init_mode);
}
void graphics_context_set_default_drawing_state(GContext *ctx,
GContextInitializationMode init_mode) {
PBL_ASSERTN(ctx);
GBitmap* bitmap = graphics_context_get_bitmap(ctx);
ctx->draw_state = (GDrawState) {
.stroke_color = GColorBlack,
.fill_color = GColorBlack,
.text_color = GColorWhite,
.tint_color = GColorWhite,
.compositing_mode = GCompOpAssign,
.clip_box = bitmap->bounds,
.drawing_box = bitmap->bounds,
#if PBL_COLOR
.antialiased = !process_manager_compiled_with_legacy2_sdk(),
#endif
.stroke_width = 1,
.draw_implementation = &g_default_draw_implementation,
.avoid_text_orphans = (init_mode == GContextInitializationMode_System),
};
}
GDrawState graphics_context_get_drawing_state(GContext* ctx) {
PBL_ASSERTN(ctx);
return ctx->draw_state;
}
void graphics_context_set_drawing_state(GContext* ctx, GDrawState draw_state) {
PBL_ASSERTN(ctx);
ctx->draw_state = draw_state;
}
void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {
PBL_ASSERTN(ctx);
ctx->draw_state.drawing_box.origin = gpoint_add(ctx->draw_state.drawing_box.origin, offset);
}
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
#if PBL_BW
color = gcolor_get_bw(color);
#else
color = gcolor_closest_opaque(color);
#endif
ctx->draw_state.stroke_color = color;
}
void graphics_context_set_stroke_color_2bit(GContext* ctx, GColor2 color) {
graphics_context_set_stroke_color(ctx, get_native_color(color));
}
void graphics_context_set_fill_color(GContext* ctx, GColor color) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
#if PBL_BW
color = gcolor_get_grayscale(color);
#else
color = gcolor_closest_opaque(color);
#endif
ctx->draw_state.fill_color = color;
}
void graphics_context_set_fill_color_2bit(GContext* ctx, GColor2 color) {
graphics_context_set_fill_color(ctx, get_native_color(color));
}
void graphics_context_set_text_color(GContext* ctx, GColor color) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
#if PBL_BW
color = gcolor_get_bw(color);
#else
color = gcolor_closest_opaque(color);
#endif
ctx->draw_state.text_color = color;
}
void graphics_context_set_text_color_2bit(GContext* ctx, GColor2 color) {
graphics_context_set_text_color(ctx, get_native_color(color));
}
void graphics_context_set_tint_color(GContext *ctx, GColor color) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
ctx->draw_state.tint_color = gcolor_closest_opaque(color);
}
void graphics_context_set_compositing_mode(GContext* ctx, GCompOp mode) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
ctx->draw_state.compositing_mode = mode;
}
void graphics_context_set_antialiased(GContext* ctx, bool enable) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
#if PBL_COLOR
ctx->draw_state.antialiased = enable;
#endif
}
bool graphics_context_get_antialiased(GContext *ctx) {
PBL_ASSERTN(ctx);
return PBL_IF_COLOR_ELSE(ctx->draw_state.antialiased, false);
}
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
// Ignore if stroke width == 0
if (stroke_width >= 1) {
ctx->draw_state.stroke_width = stroke_width;
}
}
GSize graphics_context_get_framebuffer_size(GContext *ctx) {
if (ctx && ctx->parent_framebuffer) {
return ctx->parent_framebuffer->size;
} else {
return GSize(DISP_COLS, DISP_ROWS);
}
}
GBitmap* graphics_context_get_bitmap(GContext* ctx) {
PBL_ASSERTN(ctx);
return &ctx->dest_bitmap;
}
void graphics_context_mark_dirty_rect(GContext* ctx, GRect rect) {
PBL_ASSERTN(ctx);
if (ctx->parent_framebuffer) {
framebuffer_mark_dirty_rect(ctx->parent_framebuffer, rect);
}
}
bool graphics_frame_buffer_is_captured(GContext* ctx) {
PBL_ASSERTN(ctx);
return ctx->lock;
}
GBitmap* graphics_capture_frame_buffer_format(GContext *ctx, GBitmapFormat format) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
APP_LOG(APP_LOG_LEVEL_WARNING,
"Frame buffer has already been captured; it cannot be captured again until "
"graphics_release_frame_buffer has been called.");
return NULL;
}
ctx->lock = true;
GBitmap *native = graphics_context_get_bitmap(ctx);
if (format == native->info.format) {
return native;
}
GBitmap *result = NULL;
if (format == GBitmapFormat1Bit && native->info.format == GBitmapFormat8Bit) {
// Create a new blank gbitmap in the correct format.
const GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
if (process_manager_compiled_with_legacy2_sdk()) {
result = app_state_legacy2_get_2bit_framebuffer();
} else {
result = gbitmap_create_blank(native_framebuffer->bounds.size, GBitmapFormat1Bit);
}
if (result) {
prv_graphics_convert_8_bit_to_1_bit(native_framebuffer, result);
}
}
if (!result) {
ctx->lock = false;
}
return result;
}
GBitmap* graphics_capture_frame_buffer_2bit(GContext *ctx) {
return graphics_capture_frame_buffer_format(ctx, GBitmapFormat1Bit);
}
MOCKABLE GBitmap *graphics_capture_frame_buffer(GContext *ctx) {
PBL_ASSERTN(ctx);
return graphics_capture_frame_buffer_format(ctx, GBITMAP_NATIVE_FORMAT);
}
#include "system/profiler.h"
MOCKABLE bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
PBL_ASSERTN(ctx);
GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
if (gbitmap_get_format(buffer) != GBITMAP_NATIVE_FORMAT) {
ctx->lock = false;
bitblt_bitmap_into_bitmap(native_framebuffer, buffer, GPointZero,
GCompOpAssign, GColorWhite);
framebuffer_dirty_all(ctx->parent_framebuffer);
// Don't destroy the bitmap we got from app_state_legacy2_get_2bit_framebuffer()
if (!process_manager_compiled_with_legacy2_sdk()) {
gbitmap_destroy(buffer);
}
return true;
}
if (buffer == native_framebuffer) {
ctx->lock = false;
framebuffer_dirty_all(ctx->parent_framebuffer);
return true;
}
return false;
}

View file

@ -0,0 +1,137 @@
/*
* 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.
*/
#pragma once
#include "gcontext.h"
#include "gtypes.h"
#include "graphics_bitmap.h"
#include "graphics_circle.h"
#include "graphics_line.h"
//! @file graphics/graphics.h
//! Defines the base graphics subsystem including the screen buffer. Users of these
//! functions should call graphics_set_pixel to draw to the memory-backed buffer, and
//! then graphics_flush to actually apply these changes to the display.
//! @addtogroup Graphics
//! @{
typedef struct FrameBuffer FrameBuffer;
//! @addtogroup Drawing Drawing Primitives
//! \brief Functions to draw into a graphics context
//!
//! Use these drawing functions inside a Layer's `.update_proc` drawing
//! callback. A `GContext` is passed into this callback as an argument.
//! This `GContext` can then be used with all of the drawing functions which
//! are documented below.
//! See \ref GraphicsContext for more information about the graphics context.
//!
//! Refer to \htmlinclude UiFramework.html (chapter "Layers" and "Graphics") for a
//! conceptual overview of the drawing system, Layers and relevant code examples.
//!
//! Other drawing functions and related documentation:
//! * \ref TextDrawing
//! * \ref PathDrawing
//! * \ref GraphicsTypes
//! @{
//! Draws a pixel at given point in the current stroke color
//! @param ctx The destination graphics context in which to draw
//! @param point The point at which to draw the pixel
void graphics_draw_pixel(GContext* ctx, GPoint point);
//! Fills a rectangle with the current fill color
//! @param ctx The destination graphics context in which to draw
//! @param rect The rectangle to fill
//! @see graphics_fill_round_rect
void graphics_fill_rect(GContext *ctx, const GRect *rect);
//! Draws a 1-pixel wide rectangle outline in the current stroke color
//! @param ctx The destination graphics context in which to draw
//! @param rect The rectangle for which to draw the outline
void graphics_draw_rect_by_value(GContext *ctx, GRect rect);
void graphics_draw_rect(GContext *ctx, const GRect *rect);
void graphics_draw_rect_precise(GContext* ctx, const GRectPrecise *rect);
//! Fills a rectangle with the current fill color, optionally rounding all or a
//! selection of its corners.
//! @param ctx The destination graphics context in which to draw
//! @param rect The rectangle to fill
//! @param corner_radius The rounding radius of the corners in pixels (maximum is 8 pixels)
//! @param corner_mask Bitmask of the corners that need to be rounded.
//! @see \ref GCornerMask
void graphics_fill_round_rect_by_value(GContext *ctx, GRect rect, uint16_t corner_radius,
GCornerMask corner_mask);
void graphics_fill_round_rect(GContext *ctx, const GRect *rect, uint16_t corner_radius,
GCornerMask corner_mask);
//! Draws the outline of a rounded rectangle in the current stroke color
//! @param ctx The destination graphics context in which to draw
//! @param rect The rectangle defining the dimensions of the rounded rectangle to draw
//! @param radius The corner radius in pixels
void graphics_draw_round_rect_by_value(GContext *ctx, GRect rect, uint16_t radius);
void graphics_draw_round_rect(GContext *ctx, const GRect *rect, uint16_t radius);
//! Whether or not the frame buffer has been captured by {@link graphics_capture_frame_buffer}.
//! Graphics functions will not affect the frame buffer until it has been released by
//! {@link graphics_release_frame_buffer}.
//! @param ctx The graphics context providing the frame buffer
//! @return True if the frame buffer has been captured
bool graphics_frame_buffer_is_captured(GContext* ctx);
//! Captures the frame buffer for direct access, using the given format.
//! Graphics functions will not affect the frame buffer while it is captured.
//! The frame buffer is released when {@link graphics_release_frame_buffer} is called.
//! The frame buffer must be released before the end of a layer's `.update_proc`
//! for the layer to be drawn properly.
//!
//! While the frame buffer is captured calling {@link graphics_capture_frame_buffer}
//! will fail and return `NULL`.
//! @note When writing to the frame buffer, you should respect the visible boundaries of a
//! window on the screen. Use layer_get_frame(window_get_root_layer(window)).origin to obtain its
//! position relative to the frame buffer. For example, drawing to (5, 5) in the frame buffer
//! while the window is transitioning to the left with its origin at (-20, 0) would
//! effectively draw that point at (25, 5) relative to the window. For this reason you should
//! consider the window's root layer frame when calculating drawing coordinates.
//! @see GBitmap
//! @see GBitmapFormat
//! @see layer_get_frame
//! @see window_get_root_layer
//! @param ctx The graphics context providing the frame buffer
//! @param format The format in which the framebuffer should be captured. Supported formats
//! are \ref GBitmapFormat1Bit and \ref GBitmapFormat8Bit.
//! @return A pointer to the frame buffer. `NULL` if failed.
GBitmap *graphics_capture_frame_buffer_format(GContext *ctx, GBitmapFormat format);
//! A shortcut to capture the framebuffer in the native format of the watch.
//! @see graphics_capture_frame_buffer_format
GBitmap* graphics_capture_frame_buffer(GContext* ctx);
GBitmap* graphics_capture_frame_buffer_2bit(GContext* ctx);
//! Releases the frame buffer.
//! Must be called before the end of a layer's `.update_proc` for the layer to be drawn properly.
//!
//! If `buffer` does not point to the address previously returned by
//! {@link graphics_capture_frame_buffer} the frame buffer will not be released.
//! @param ctx The graphics context providing the frame buffer
//! @param buffer The pointer to frame buffer
//! @return True if the frame buffer was released successfully
bool graphics_release_frame_buffer(GContext* ctx, GBitmap* buffer);
//! @} // end addtogroup Drawing
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,334 @@
/*
* 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_bitmap.h"
#include "bitblt.h"
#include "bitblt_private.h"
#include "gcontext.h"
#include "graphics.h"
#include "graphics_private.h"
#include "system/passert.h"
#include "util/graphics.h"
#include "util/trig.h"
void graphics_draw_bitmap_in_rect_processed(GContext *ctx, const GBitmap *src_bitmap,
const GRect *rect_ref, GBitmapProcessor *processor) {
if (!ctx || ctx->lock || !rect_ref) {
return;
}
// Make a copy of the rect and translate it to global screen coordinates
GRect rect = *rect_ref;
rect.origin = gpoint_add(rect.origin, ctx->draw_state.drawing_box.origin);
// Store the bitmap to draw in a new pointer that the processor can modify if it wants to
const GBitmap *bitmap_to_draw = src_bitmap;
// Call the processor's pre function, if applicable
if (processor && processor->pre) {
processor->pre(processor, ctx, &bitmap_to_draw, &rect);
}
// Bail out early if the bitmap to draw is NULL
if (!bitmap_to_draw) {
// Set rect to GRectZero so the processor's .post function knows that nothing was drawn
rect = GRectZero;
goto call_processor_post_function_and_return;
}
// TODO PBL-35694: what if src_bitmap == dest_bitmap....
// This currently works only if the regions are equal, or the dest region is
// to the bottom/right of it, since we scan from left to right, top to bottom
GBitmap *dest_bitmap = graphics_context_get_bitmap(ctx);
PBL_ASSERTN(dest_bitmap);
// Save the original origin to compensate the position within src when rect.origin is negative
const GPoint unclipped_origin = rect.origin;
// Clip the rect to avoid drawing outside of the bitmap memory
grect_standardize(&rect);
grect_clip(&rect, &dest_bitmap->bounds);
grect_clip(&rect, &ctx->draw_state.clip_box);
// Bail out early if the clipped drawing rectangle is empty
if (grect_is_empty(&rect)) {
goto call_processor_post_function_and_return;
}
// Calculate the offset of src_bitmap to use
const GPoint src_offset = gpoint_sub(rect.origin, unclipped_origin);
// Blit bitmap_to_draw (which might have been changed by the processor) into dest_bitmap
bitblt_bitmap_into_bitmap_tiled(dest_bitmap, bitmap_to_draw, rect, src_offset,
ctx->draw_state.compositing_mode, ctx->draw_state.tint_color);
// Mark the region where the bitmap was drawn as dirty
graphics_context_mark_dirty_rect(ctx, rect);
call_processor_post_function_and_return:
// Call the processor's post function, if applicable
if (processor && processor->post) {
processor->post(processor, ctx, bitmap_to_draw, &rect);
}
}
void graphics_draw_bitmap_in_rect(GContext *ctx, const GBitmap *src_bitmap, const GRect *rect_ref) {
graphics_draw_bitmap_in_rect_processed(ctx, src_bitmap, rect_ref, NULL);
}
void graphics_draw_bitmap_in_rect_by_value(GContext *ctx, const GBitmap *src_bitmap, GRect rect) {
graphics_draw_bitmap_in_rect_processed(ctx, src_bitmap, &rect, NULL);
}
typedef struct DivResult {
int32_t quot;
int32_t rem;
} DivResult;
//! a div and mod operation where any remainder will always be the same direction as the numerator
static DivResult polar_div(int32_t numer, int32_t denom) {
DivResult res;
res.quot = numer / denom;
res.rem = numer % denom;
if (numer < 0 && res.rem > 0) {
res.rem -= denom;
res.quot += denom;
}
return res;
}
#if PBL_BW
T_STATIC bool get_bitmap_bit(GBitmap *bmp, int x, int y) {
int byte_num = y * bmp->row_size_bytes + x / 8;
int bit_num = x % 8;
uint8_t byte = ((uint8_t*)(bmp->addr))[byte_num];
return (byte & (1 << bit_num)) ? 1 : 0;
}
#elif PBL_COLOR
T_STATIC GColor get_bitmap_color(GBitmap *bmp, int x, int y) {
const GBitmapFormat format = gbitmap_get_format(bmp);
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bmp, y);
const uint8_t *src = row_info.data;
const uint8_t src_bpp = gbitmap_get_bits_per_pixel(format);
uint8_t cindex = raw_image_get_value_for_bitdepth(src, x,
0, // y = 0 when using data_row
bmp->row_size_bytes,
src_bpp);
// Default color to be the raw color index - update only if palletized
GColor src_color = (GColor){.argb = cindex};
bool palletized = ((format == GBitmapFormat1BitPalette) ||
(format == GBitmapFormat2BitPalette) ||
(format == GBitmapFormat4BitPalette));
if (palletized) {
// Look up color in pallete if palletized
const GColor *palette = bmp->palette;
src_color = palette[cindex];
}
return src_color;
}
#endif
void graphics_draw_rotated_bitmap(GContext* ctx, GBitmap *src, GPoint src_ic, int rotation,
GPoint dest_ic) {
PBL_ASSERTN(ctx);
if (rotation == 0) {
graphics_draw_bitmap_in_rect(
ctx, src, &(GRect){ .origin = { dest_ic.x - src_ic.x, dest_ic.y - src_ic.y },
.size = src->bounds.size });
return;
}
GBitmap *dest_bitmap = graphics_capture_frame_buffer(ctx);
if (dest_bitmap == NULL) {
return;
}
GRect dest_clip = ctx->draw_state.clip_box;
dest_ic.x += ctx->draw_state.drawing_box.origin.x;
dest_ic.y += ctx->draw_state.drawing_box.origin.y;
GCompOp compositing_mode = ctx->draw_state.compositing_mode;
#if PBL_BW
GColor foreground, background;
switch (compositing_mode) {
case GCompOpAssign:
foreground = GColorWhite;
background = GColorBlack;
break;
case GCompOpAssignInverted:
foreground = GColorBlack;
background = GColorWhite;
break;
case GCompOpOr:
foreground = GColorWhite;
background = GColorClear;
break;
case GCompOpAnd:
foreground = GColorClear;
background = GColorBlack;
break;
case GCompOpClear:
foreground = GColorBlack;
background = GColorClear;
break;
case GCompOpSet:
foreground = GColorClear;
background = GColorWhite;
break;
default:
PBL_ASSERT(0, "unknown coposting mode %d", compositing_mode);
return;
}
#endif
// Backup context color
const GColor ctx_color = ctx->draw_state.stroke_color;
if (grect_contains_point(&src->bounds, &src_ic)) {
// TODO: Optimize further (PBL-15657)
// If src_ic is within the bounds of the source image, do the following performance
// optimization:
// Create a clipping rectangle based on the max distance away from the pivot point
// that the destination image could be located at:
// max distance from the pivot point = sqrt(x^2 + y^2), where x and y are at max twice the width
// and height of the source image
// i.e. in case the anchor point is on the edge then it would be twice
// Also need to account for the dest_ic offset
const int16_t max_width = MAX(src->bounds.origin.x + src->bounds.size.w - src_ic.x,
src_ic.x - src->bounds.origin.x);
const int16_t max_height = MAX(src->bounds.origin.y + src->bounds.size.h - src_ic.y,
src_ic.y - src->bounds.origin.y);
const int32_t width = 2 * (max_width + 1); // Add one more pixel in case on the edge
const int32_t height = 2 * (max_height + 1); // Add one more pixel in case on the edge
// add two pixels just in case of rounding isssues
const int32_t max_distance = integer_sqrt((width * width) + (height * height)) + 2;
const int32_t min_x = src_ic.x - max_distance;
const int32_t min_y = src_ic.y - max_distance;
const int32_t size_x = max_distance*2;
const int32_t size_y = size_x;
const GRect dest_clip_min = GRect(dest_ic.x + min_x, dest_ic.y + min_y, size_x, size_y);
grect_clip(&dest_clip, &dest_clip_min);
}
for (int y = dest_clip.origin.y; y < dest_clip.origin.y + dest_clip.size.h; ++y) {
for (int x = dest_clip.origin.x; x < dest_clip.origin.x + dest_clip.size.w; ++x) {
// only draw if within the dest range
const GBitmapDataRowInfo dest_info = gbitmap_get_data_row_info(dest_bitmap, y);
if (!WITHIN(x, dest_info.min_x, dest_info.max_x)) {
continue;
}
const int32_t cos_value = cos_lookup(-rotation);
const int32_t sin_value = sin_lookup(-rotation);
const int32_t src_numerator_x = cos_value * (x - dest_ic.x) - sin_value * (y - dest_ic.y);
const int32_t src_numerator_y = cos_value * (y - dest_ic.y) + sin_value * (x - dest_ic.x);
const DivResult src_vector_x = polar_div(src_numerator_x, TRIG_MAX_RATIO);
const DivResult src_vector_y = polar_div(src_numerator_y, TRIG_MAX_RATIO);
const int32_t src_x = src_ic.x + src_vector_x.quot;
const int32_t src_y = src_ic.y + src_vector_y.quot;
// only draw if within the src range
const GBitmapDataRowInfo src_info = gbitmap_get_data_row_info(src, src_y);
if (!(WITHIN(src_x, 0, src->bounds.size.w - 1) &&
WITHIN(src_y, 0, src->bounds.size.h - 1) &&
WITHIN(src_x, src_info.min_x, src_info.max_x))) {
continue;
}
#if PBL_BW
// dividing by 8 to avoid overflows of <thresh> in the next loop
const int32_t horiz_contrib[3] = {
src_vector_x.rem < 0 ? (-src_vector_x.rem) >> 3 : 0,
src_vector_x.rem < 0 ? (TRIG_MAX_RATIO + src_vector_x.rem) >> 3 :
(TRIG_MAX_RATIO - src_vector_x.rem) >> 3,
src_vector_x.rem < 0 ? 0 : (src_vector_x.rem) >> 3
};
const int32_t vert_contrib[3] = {
src_vector_y.rem < 0 ? (-src_vector_y.rem) >> 3 : 0,
src_vector_y.rem < 0 ? (TRIG_MAX_RATIO + src_vector_y.rem) >> 3 :
(TRIG_MAX_RATIO - src_vector_y.rem) >> 3,
src_vector_y.rem < 0 ? 0 : (src_vector_y.rem) >> 3
};
int32_t thresh = 0;
for (int i = -1; i <= 1; ++i) {
for (int j = -1; j <= 1; ++j) {
if (src_x + i >= 0 && src_x + i < src->bounds.size.w
&& src_y + j >= 0 && src_y + j < src->bounds.size.h) {
// I'm within bounds
if (get_bitmap_bit(src, src_x + i , src_y + j)) {
// more color
thresh += (horiz_contrib[i+1] * vert_contrib[j+1]);
} else {
// less color
thresh -= (horiz_contrib[i+1] * vert_contrib[j+1]);
}
}
}
}
if (thresh > 0) {
ctx->draw_state.stroke_color = foreground;
} else {
ctx->draw_state.stroke_color = background;
}
if (!gcolor_is_transparent(ctx->draw_state.stroke_color)) {
graphics_private_set_pixel(ctx, GPoint(x, y));
}
#elif PBL_COLOR
const GColor src_color = get_bitmap_color(src, src_x, src_y);
const GColor tint_color = ctx->draw_state.tint_color;
switch (compositing_mode) {
case GCompOpSet: {
const GColor dst_color = get_bitmap_color(dest_bitmap, x, y);
ctx->draw_state.stroke_color = gcolor_alpha_blend(src_color, dst_color);
break;
}
case GCompOpOr: {
const GColor dst_color = get_bitmap_color(dest_bitmap, x, y);
if (tint_color.a != 0) {
GColor actual_color = tint_color;
actual_color.a = src_color.a;
ctx->draw_state.stroke_color = gcolor_alpha_blend(actual_color, dst_color);
break;
}
}
case GCompOpAssign:
default:
// Do assign by default
ctx->draw_state.stroke_color = src_color;
break;
}
ctx->draw_state.stroke_color.a = 3; // Force to be opaque
graphics_private_set_pixel(ctx, GPoint(x, y));
#endif
}
}
// Restore context color
ctx->draw_state.stroke_color = ctx_color;
graphics_release_frame_buffer(ctx, dest_bitmap);
}

View file

@ -0,0 +1,67 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
//! Draws a bitmap into the graphics context, inside the specified rectangle, using the specified
//! processor.
//! @param ctx The destination graphics context in which to draw the bitmap
//! @param bitmap The bitmap to draw
//! @param rect The rectangle in which to draw the bitmap
//! @param processor Optional processor to use in drawing the bitmap
//! @note If the size of `rect` is smaller than the size of the bitmap,
//! the bitmap will be clipped on right and bottom edges.
//! If the size of `rect` is larger than the size of the bitmap,
//! the bitmap will be tiled automatically in both horizontal and vertical
//! directions, effectively drawing a repeating pattern.
//! @see GBitmap
//! @see GContext
//! @internal
//! @see app_get_current_graphics_context
void graphics_draw_bitmap_in_rect_processed(GContext *ctx, const GBitmap *bitmap,
const GRect *rect, GBitmapProcessor *processor);
//! Draws a bitmap into the graphics context, inside the specified rectangle
//! @param ctx The destination graphics context in which to draw the bitmap
//! @param bitmap The bitmap to draw
//! @param rect The rectangle in which to draw the bitmap
//! @note If the size of `rect` is smaller than the size of the bitmap,
//! the bitmap will be clipped on right and bottom edges.
//! If the size of `rect` is larger than the size of the bitmap,
//! the bitmap will be tiled automatically in both horizontal and vertical
//! directions, effectively drawing a repeating pattern.
//! @see GBitmap
//! @see GContext
//! @internal
//! @see app_get_current_graphics_context
void graphics_draw_bitmap_in_rect_by_value(GContext *ctx, const GBitmap *bitmap, GRect rect);
void graphics_draw_bitmap_in_rect(GContext *ctx, const GBitmap *bitmap, const GRect *rect);
//! Draws a rotated bitmap with a memory-sensitive 2x anti-aliasing technique
//! (using ray-finding instead of super-sampling), which is thresholded into a b/w bitmap for 1-bit
//! and color blended for 8-bit.
//! @note This API has performance limitations that can degrade user experience. Use sparingly.
//! @param ctx The destination graphics context in which to draw
//! @param src The source bitmap to draw
//! @param src_ic Instance center (single point unaffected by rotation) relative to source bitmap
//! @param rotation Angle of rotation. Rotation is an integer between 0 (no rotation)
//! and TRIG_MAX_ANGLE (360 degree rotation). Use \ref DEG_TO_TRIGANGLE to easily convert degrees
//! to the appropriate value.
//! @param dest_ic Where to draw the instance center of the rotated bitmap in the context.
void graphics_draw_rotated_bitmap(GContext* ctx, GBitmap *src, GPoint src_ic, int rotation,
GPoint dest_ic);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,167 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
//! @internal
//! Draws a quadrant of a circle based on what is set in the context for stroke width and
//! antialiasing.
void graphics_circle_quadrant_draw(GContext* ctx, GPoint p, uint16_t radius, GCornerMask quadrant);
//! @internal
//! Fills an antialiased circle in quadrants
MOCKABLE void graphics_internal_circle_quadrant_fill_aa(GContext* ctx, GPoint p,
uint16_t radius, GCornerMask quadrant);
//! @internal
//! Fills a non-antialiased circle in quadrants
void graphics_circle_quadrant_fill_non_aa(GContext* ctx, GPoint p,
uint16_t radius, GCornerMask quadrant);
//! @internal
//! Fills a non-antialiased circle
MOCKABLE void graphics_circle_fill_non_aa(GContext* ctx, GPoint p, uint16_t radius);
//! @internal
//! Draws an arc with fixed-point precision
void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center, Fixed_S16_3 radius,
int32_t angle_start, int32_t angle_end);
//! @internal
//! Precise version of graphics_fill_radial_internal
void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
int32_t angle_start, int32_t angle_end);
//! @addtogroup Graphics
//! @{
//! @addtogroup Drawing Drawing Primitives
//! @{
//! Draws the outline of a circle in the current stroke color
//! @param ctx The destination graphics context in which to draw
//! @param p The center point of the circle
//! @param radius The radius in pixels
void graphics_draw_circle(GContext* ctx, GPoint p, uint16_t radius);
//! Fills a circle in the current fill color
//! @param ctx The destination graphics context in which to draw
//! @param p The center point of the circle
//! @param radius The radius in pixels
void graphics_fill_circle(GContext* ctx, GPoint p, uint16_t radius);
//! Values to specify how a given rectangle should be used to derive an oval shape.
//! @see \ref graphics_fill_radial_internal
//! @see \ref graphics_draw_arc_internal
//! @see \ref gpoint_from_polar_internal
//! @see \ref grect_centered_from_polar
typedef enum {
//! Places a circle at the center of the rectangle, with a diameter that matches
//! the rectangle's shortest side.
GOvalScaleModeFitCircle,
//! Places a circle at the center of the rectangle, with a diameter that matches
//! the rectangle's longest side.
//! The circle may overflow the bounds of the rectangle.
GOvalScaleModeFillCircle,
} GOvalScaleMode;
//! Draws a line arc clockwise between `angle_start` and `angle_end`, where 0° is
//! the top of the circle. If the difference between `angle_start` and `angle_end` is greater
//! than 360°, a full circle will be drawn.
//! @param ctx The destination graphics context in which to draw using the current
//! stroke color and antialiasing setting.
//! @param rect The reference rectangle to derive the center point and radius (see scale_mode).
//! @param scale_mode Determines how rect will be used to derive the center point and radius.
//! @param angle_start Radial starting angle. Use \ref DEG_TO_TRIGANGLE to easily convert degrees
//! to the appropriate value.
//! @param angle_end Radial finishing angle. If smaller than `angle_start`, nothing will be drawn.
void graphics_draw_arc(GContext *ctx, GRect rect, GOvalScaleMode scale_mode,
int32_t angle_start, int32_t angle_end);
//! @internal
void graphics_draw_arc_internal(GContext *ctx, GPoint center, uint16_t radius, int32_t angle_start,
int32_t angle_end);
//! Fills a circle clockwise between `angle_start` and `angle_end`, where 0° is
//! the top of the circle. If the difference between `angle_start` and `angle_end` is greater
//! than 360°, a full circle will be drawn and filled. If `angle_start` is greater than
//! `angle_end` nothing will be drawn.
//! @note A simple example is drawing a 'Pacman' shape, with a starting angle of -225°, and
//! ending angle of 45°. By setting `inset_thickness` to a non-zero value (such as 30) this
//! example will produce the letter C.
//! @param ctx The destination graphics context in which to draw using the current
//! fill color and antialiasing setting.
//! @param rect The reference rectangle to derive the center point and radius (see scale).
//! @param scale_mode Determines how rect will be used to derive the center point and radius.
//! @param inset_thickness Describes how thick in pixels the radial will be drawn towards its
//! center measured from the outside.
//! @param angle_start Radial starting angle. Use \ref DEG_TO_TRIGANGLE to easily convert degrees
//! to the appropriate value.
//! @param angle_end Radial finishing angle. If smaller than `angle_start`, nothing will be drawn.
void graphics_fill_radial(GContext *ctx, GRect rect, GOvalScaleMode scale_mode,
uint16_t inset_thickness,
int32_t angle_start, int32_t angle_end);
//! @internal
void graphics_fill_radial_internal(GContext *ctx, GPoint center, uint16_t radius_inner,
uint16_t radius_outer, int32_t angle_start, int32_t angle_end);
//! @internal
void graphics_fill_oval(GContext *ctx, GRect rect, GOvalScaleMode scale_mode);
//! Calculates a GPoint located at the angle provided on the perimeter of a circle defined by the
//! provided GRect.
//! @param rect The reference rectangle to derive the center point and radius (see scale_mode).
//! @param scale_mode Determines how rect will be used to derive the center point and radius.
//! @param angle The angle at which the point on the circle's perimeter should be calculated.
//! Use \ref DEG_TO_TRIGANGLE to easily convert degrees to the appropriate value.
//! @return The point on the circle's perimeter.
GPoint gpoint_from_polar(GRect rect, GOvalScaleMode scale_mode, int32_t angle);
//! @internal
GPoint gpoint_from_polar_internal(const GPoint *center, uint16_t radius, int32_t angle);
//! @internal
GPointPrecise gpoint_from_polar_precise(const GPointPrecise *precise_center,
uint16_t precise_radius, int32_t angle);
//! Calculates a rectangle centered on the perimeter of a circle at a given angle.
//! Use this to construct rectangles that follow the perimeter of a circle as an input for
//! \ref graphics_fill_radial_internal or \ref graphics_draw_arc_internal,
//! e.g. to draw circles every 30 degrees on a watchface.
//! @param rect The reference rectangle to derive the circle's center point and radius (see
//! scale_mode).
//! @param scale_mode Determines how rect will be used to derive the circle's center point and
//! radius.
//! @param angle The angle at which the point on the circle's perimeter should be calculated.
//! Use \ref DEG_TO_TRIGANGLE to easily convert degrees to the appropriate value.
//! @param size Width and height of the desired rectangle.
//! @return The rectangle centered on the circle's perimeter.
GRect grect_centered_from_polar(GRect rect, GOvalScaleMode scale_mode, int32_t angle, GSize size);
//! @internal
//! Calculates a center point and radius from a given rect and scale mode
void grect_polar_calc_values(const GRect *rect, GOvalScaleMode scale_mode, GPointPrecise *center,
Fixed_S16_3 *radius);
//! @internal
//! Returns a GRect with a given size that's centered at center
GRect grect_centered_internal(const GPointPrecise *center, GSize size);
//! @} // end addtogroup Drawing
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,56 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
// For arc/radial fill algorithms
#define QUADRANTS_NUM 4 // Just in case of fluctuation
#define QUADRANT_ANGLE (TRIG_MAX_ANGLE / QUADRANTS_NUM)
static GCornerMask radius_quadrants[QUADRANTS_NUM] =
{ GCornerTopRight, GCornerBottomRight, GCornerBottomLeft, GCornerTopLeft };
typedef struct {
int32_t angle;
GCornerMask quadrant;
} EllipsisPartDrawConfig;
typedef struct {
EllipsisPartDrawConfig start_quadrant;
GCornerMask full_quadrants;
EllipsisPartDrawConfig end_quadrant;
} EllipsisDrawConfig;
typedef struct {
GCornerMask mask;
int8_t x_mul;
int8_t y_mul;
} GCornerMultiplier;
static GCornerMultiplier quadrant_mask_mul[] = {
{GCornerTopRight, 1, -1},
{GCornerBottomRight, 1, 1},
{GCornerBottomLeft, -1, 1},
{GCornerTopLeft, -1, -1}
};
T_STATIC EllipsisDrawConfig prv_calc_draw_config_ellipsis(int32_t angle_start, int32_t angle_end);
void prv_fill_oval_quadrant(GContext *ctx, GPoint point,
uint16_t outer_radius_x, uint16_t outer_radius_y,
uint16_t inner_radius_x, uint16_t inner_radius_y,
GCornerMask quadrant);

View file

@ -0,0 +1,830 @@
/*
* 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 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 corrseponding 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 neccessary
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 substracted 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 straigth 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 substracted 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 straigth 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 suppported 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);
}

View file

@ -0,0 +1,83 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
//! @addtogroup Graphics
//! @{
//! @addtogroup Drawing Drawing Primitives
//! @{
//! Draws line in the current stroke color, current stroke width and AA flag
//! @param ctx The destination graphics context in which to draw
//! @param p0 The starting point of the line
//! @param p1 The ending point of the line
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1);
//! @} // end addtogroup Drawing
//! @} // end addtogroup Graphics
//! @internal
//! Draws non-antialiased 1px width line between given points, will adjust to drawing_box
MOCKABLE void graphics_line_draw_1px_non_aa(GContext* ctx, GPoint p0, GPoint p1);
//! @internal
//! Draws antialiased 1px width line between given points, will adjust to drawing box
MOCKABLE void graphics_line_draw_1px_aa(GContext* ctx, GPoint p0, GPoint p1);
//! @internal
//! Draws antialiased stroked line between given points, will adjust for drawing_box
//! @note This only supports odd numbers for stroke_width - even numbers will be rounded up.
//! Minimal supported stroke_width is 3
MOCKABLE void graphics_line_draw_stroked_aa(GContext* ctx, GPoint p0, GPoint p1,
uint8_t stroke_width);
//! @internal
//! Draws non-antialiased stroked line between given precise points, will adjust for drawing_box
//! Minimal supported stroke_width is 2
MOCKABLE void graphics_line_draw_precise_stroked_non_aa(GContext* ctx, GPointPrecise p0,
GPointPrecise p1, uint8_t stroke_width);
//! @internal
//! Draws antialiased stroked line between given precise points, will adjust for drawing_box
//! Minimal supported stroke_width is 2
MOCKABLE void graphics_line_draw_precise_stroked_aa(GContext* ctx, GPointPrecise p0,
GPointPrecise p1, uint8_t stroke_width);
//! @internal
//! Draws non-antialiased stroked line between given point, will adjust for drawing_box
//! @note This only supports odd numbers for stroke_width - even numbers will be rounded up.
//! Minimal supported stroke_width is 3
MOCKABLE void graphics_line_draw_stroked_non_aa(GContext* ctx, GPoint p0, GPoint p1,
uint8_t stroke_width);
//! @internal
//! Draws stroked line between given precise points, will adjust for drawing_box,
//! current stroke color, current stroke width and AA flag
//! Minimal supported stroke_width is 2
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1);
//! @internal
//! Draws a 1 pixel wide non-antialiased vertical dotted line of length pixels starting at p0.
//! Will draw the line in the positive y direction. Will adjust for drawing_box.
void graphics_draw_vertical_line_dotted(GContext* ctx, GPoint p0, uint16_t length);
//! @internal
//! Draws a 1 pixel high non-antialiased horizontal dotted line of length pixels starting at p0.
//! Will draw the line in the positive x direction. Will adjust for drawing_box.
void graphics_draw_horizontal_line_dotted(GContext* ctx, GPoint p0, uint16_t length);

View file

@ -0,0 +1,129 @@
/*
* 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 "gcontext.h"
#include "graphics_private_raw.h"
#include "graphics_private_raw_mask.h"
#include "applib/applib_malloc.auto.h"
#include <string.h>
GDrawMask *graphics_context_mask_create(const GContext *ctx, bool transparent) {
#if CAPABILITY_HAS_MASKING
if (!ctx) {
return NULL;
}
const GBitmap *framebuffer_bitmap = &ctx->dest_bitmap;
const int framebuffer_bitmap_height = framebuffer_bitmap->bounds.size.h;
const size_t num_bytes_needed_for_mask_row_infos =
sizeof(GDrawMaskRowInfo) * framebuffer_bitmap_height;
// Iterate over framebuffer data row infos to calculate the Bytes needed for the pixel_mask_data
size_t num_pixels = 0;
for (int y = 0; y < framebuffer_bitmap_height; y++) {
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(framebuffer_bitmap, (uint16_t)y);
const int row_width = row_info.max_x - row_info.min_x + 1;
num_pixels += row_width;
}
// Round up after dividing by the mask bits per pixel
const size_t num_bytes_needed_for_pixel_mask_data = DIVIDE_CEIL(num_pixels,
GDRAWMASK_BITS_PER_PIXEL);
GDrawMask *result = applib_zalloc(
sizeof(*result) + num_bytes_needed_for_mask_row_infos + num_bytes_needed_for_pixel_mask_data);
if (result) {
// We store the mask_row_infos first in the .data buffer, followed by the pixel_mask_data
*result = (GDrawMask) {
.mask_row_infos = (GDrawMaskRowInfo *)result->data,
.pixel_mask_data = ((uint8_t *)result->data) + num_bytes_needed_for_mask_row_infos,
};
// Initialize the mask according to the `transparent` argument
const uint8_t pixel_data_initial_byte_value = transparent ? (uint8_t)0b00000000 :
(uint8_t)0b11111111;
memset(result->pixel_mask_data, pixel_data_initial_byte_value,
num_bytes_needed_for_pixel_mask_data);
// Initialize the mask row infos
const uint16_t fixed_s16_s3_fraction_max_value = FIXED_S16_3_FACTOR - 1;
for (int y = 0; y < framebuffer_bitmap_height; y++) {
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(framebuffer_bitmap,
(uint16_t)y);
result->mask_row_infos[y] = (GDrawMaskRowInfo) {
.type = transparent ? GDrawMaskRowInfoType_SemiTransparent : GDrawMaskRowInfoType_Opaque,
.min_x.integer = row_info.min_x,
.min_x.fraction = (uint16_t)(transparent ? fixed_s16_s3_fraction_max_value : 0),
.max_x.integer = row_info.max_x,
.max_x.fraction = (uint16_t)(transparent ? 0 : fixed_s16_s3_fraction_max_value),
};
}
}
return result;
#else
return NULL;
#endif
}
bool graphics_context_mask_record(GContext *ctx, GDrawMask *mask) {
#if CAPABILITY_HAS_MASKING
if (!ctx) {
return false;
}
if (ctx->draw_state.draw_mask && !mask) {
// TODO PBL-33766: Update the ctx->draw_state.draw_mask's .mask_row_infos
}
const GDrawRawImplementation *draw_implementation_to_set =
mask ? &g_mask_recording_draw_implementation : &g_default_draw_implementation;
ctx->draw_state.draw_implementation = draw_implementation_to_set;
ctx->draw_state.draw_mask = mask;
return true;
#else
return false;
#endif
}
bool graphics_context_mask_use(GContext *ctx, GDrawMask *mask) {
#if CAPABILITY_HAS_MASKING
if (!ctx) {
return false;
}
// Stop any recording
graphics_context_mask_record(ctx, NULL);
// If a valid mask is set, the default draw implementation routines will respect it
ctx->draw_state.draw_mask = mask;
return true;
#else
return false;
#endif
}
void graphics_context_mask_destroy(GContext *ctx, GDrawMask *mask) {
#if CAPABILITY_HAS_MASKING
graphics_context_mask_use(ctx, NULL);
applib_free(mask);
#endif
}

View file

@ -0,0 +1,811 @@
/*
* 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 "bitblt_private.h"
#include "graphics.h"
#include "graphics_private.h"
#include "gtypes.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/math.h"
// ## Point setting/blending functions
#if PBL_COLOR
T_STATIC inline void set_pixel_raw_8bit(GContext* ctx, GPoint point) {
if (!grect_contains_point(&ctx->dest_bitmap.bounds, &point)) {
return;
}
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(&ctx->dest_bitmap, point.y);
if (!WITHIN(point.x, data_row_info.min_x, data_row_info.max_x)) {
return;
}
uint8_t *line = data_row_info.data;
GColor color = ctx->draw_state.stroke_color;
if (!gcolor_is_transparent(color)) {
// Force alpha to be opaque since that represents how framebuffer discards it in display.
// Also needed for unit tests since PNG tests interpret alpha
color.a = 3;
line[point.x] = color.argb;
}
}
#endif
#if PBL_BW
static inline void set_pixel_raw_2bit(GContext* ctx, GPoint point) {
if (!grect_contains_point(&ctx->dest_bitmap.bounds, &point)) {
return;
}
bool black = (gcolor_equal(ctx->draw_state.stroke_color, GColorBlack));
uint8_t *line = ((uint8_t *)ctx->dest_bitmap.addr) + (ctx->dest_bitmap.row_size_bytes * point.y);
bitset8_update(line, point.x, !black);
}
#endif
void graphics_private_set_pixel(GContext* ctx, GPoint point) {
if (!grect_contains_point(&ctx->draw_state.clip_box, &point)) {
return;
}
#if PBL_BW
set_pixel_raw_2bit(ctx, point);
#elif PBL_COLOR
set_pixel_raw_8bit(ctx, point);
#endif
const GRect dirty_rect = { point, { 1, 1 } };
graphics_context_mark_dirty_rect(ctx, dirty_rect);
}
// ## Private blending wrapper functions for non-aa
uint32_t graphics_private_get_1bit_grayscale_pattern(GColor color, uint8_t row_number) {
const GColor8Component luminance = (color.r + color.g + color.b) / 3;
switch (luminance) {
case 0:
return 0x00000000;
case 1:
case 2:
// This is done to create a checkerboard pattern for gray
return (row_number % 2) ? 0xAAAAAAAA : 0x55555555;
case 3:
return 0xFFFFFFFF;
default:
WTF;
}
}
void prv_assign_line_horizontal_non_aa(GContext* ctx, int16_t y, int16_t x1, int16_t x2) {
y += ctx->draw_state.drawing_box.origin.y;
x1 += ctx->draw_state.drawing_box.origin.x;
x2 += ctx->draw_state.drawing_box.origin.x;
// Clip results
const int y_min = ctx->draw_state.clip_box.origin.y;
const int y_max = grect_get_max_y(&ctx->draw_state.clip_box) - 1;
const int x_min = ctx->draw_state.clip_box.origin.x;
const int x_max = grect_get_max_x(&ctx->draw_state.clip_box) - 1;
x1 = MAX(x1, x_min);
x2 = MIN(x2, x_max);
if (!WITHIN(y, y_min, y_max) || x1 > x2) {
// Outside of drawing bounds..
return;
}
// Capture framebuffer & pass it to drawing implementation
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
ctx->draw_state.draw_implementation->blend_horizontal_line(ctx, y, x1, x2,
ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
}
void prv_assign_line_vertical_non_aa(GContext* ctx, int16_t x, int16_t y1, int16_t y2) {
x += ctx->draw_state.drawing_box.origin.x;
y1 += ctx->draw_state.drawing_box.origin.y;
y2 += ctx->draw_state.drawing_box.origin.y;
// To preserve old behaviour we add one to the end of the line about to be drawn
y2++;
// Clip results
const int y_min = ctx->draw_state.clip_box.origin.y;
const int y_max = grect_get_max_y(&ctx->draw_state.clip_box) - 1;
const int x_min = ctx->draw_state.clip_box.origin.x;
const int x_max = grect_get_max_x(&ctx->draw_state.clip_box) - 1;
y1 = MAX(y1, y_min);
y2 = MIN(y2, y_max + 1); // Thats because we added one to end of the line
if (!WITHIN(x, x_min, x_max) || y1 > y2) {
// Outside of drawing bounds..
return;
}
// Capture framebuffer & pass it to drawing implementation
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
ctx->draw_state.draw_implementation->blend_vertical_line(ctx, x, y1, y2,
ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
}
// ## Line blending wrappers:
void graphics_private_draw_horizontal_line_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, GColor color) {
if (gcolor_is_invisible(color)) {
return;
}
// look for clipbox
if (!WITHIN(y, clip_box->origin.y, grect_get_max_y(clip_box) - 1)) {
return;
}
const int16_t min_valid_x = clip_box->origin.x;
if (x1.integer < min_valid_x) {
x1 = (Fixed_S16_3){.integer = min_valid_x, .fraction = 0};
}
const int16_t max_valid_x = grect_get_max_x(clip_box) - 1;
if (x2.integer > max_valid_x) {
x2 = (Fixed_S16_3){.integer = max_valid_x};
}
// last pixel with blending (don't render the pixel if it overflows the framebuffer/clip box)
if (x2.integer >= max_valid_x) {
x2.fraction = 0;
}
ctx->draw_state.draw_implementation->assign_horizontal_line(ctx, y, x1, x2, color);
}
void graphics_private_draw_horizontal_line_integral(GContext *ctx, GBitmap *framebuffer, int16_t y,
int16_t x1, int16_t x2, GColor color) {
// This is a wrapper for prv_draw_horizontal_line_raw for integral coordintaes
// End of the line is inclusive so we subtract one
x2--;
const Fixed_S16_3 x1_fixed = Fixed_S16_3(x1 << FIXED_S16_3_PRECISION);
const Fixed_S16_3 x2_fixed = Fixed_S16_3(x2 << FIXED_S16_3_PRECISION);
ctx->draw_state.draw_implementation->assign_horizontal_line(ctx, y, x1_fixed, x2_fixed,
color);
}
void graphics_private_draw_vertical_line_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int16_t x, Fixed_S16_3 y1,
Fixed_S16_3 y2, GColor color) {
if (gcolor_is_invisible(color)) {
return;
}
// look for clipbox
if (!WITHIN(x, clip_box->origin.x, grect_get_max_x(clip_box) - 1)) {
return;
}
const int16_t min_valid_y = clip_box->origin.y;
if (y1.integer < min_valid_y) {
y1 = (Fixed_S16_3){.integer = min_valid_y, .fraction = 0};
}
const int16_t max_valid_y = grect_get_max_y(clip_box) - 1;
if (y2.integer > max_valid_y) {
y2 = (Fixed_S16_3){.integer = max_valid_y};
}
if (y1.integer > y2.integer) {
return;
}
// last pixel with blending (don't render the pixel if it overflows the framebuffer/clip box)
if (y2.integer >= max_valid_y) {
y2.fraction = 0;
}
ctx->draw_state.draw_implementation->assign_vertical_line(ctx, x, y1, y2, color);
}
void graphics_private_draw_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2) {
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
// apply draw box and clipping
x1.integer += ctx->draw_state.drawing_box.origin.x;
x2.integer += ctx->draw_state.drawing_box.origin.x;
y += ctx->draw_state.drawing_box.origin.y;
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
graphics_private_draw_horizontal_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, y,
x1, x2, ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
return;
}
#endif // PBL_COLOR
// since x1 is beginning of the line, rounding should work in favor of flooring the value
// therefore we substract one from the rounding addition to produce result similar to x2
int16_t x1_rounded =
(x1.raw_value + (FIXED_S16_3_ONE.raw_value / 2 - 1)) / FIXED_S16_3_ONE.raw_value;
int16_t x2_rounded = (x2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;
if (x1_rounded > x2_rounded) {
// AA algorithm will draw lines in one way only, so non-AA should reject those too
return;
}
prv_assign_line_horizontal_non_aa(ctx, y, x1_rounded, x2_rounded);
}
void graphics_private_draw_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1, Fixed_S16_3 y2) {
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
// apply draw box and clipping
y1.integer += ctx->draw_state.drawing_box.origin.y;
y2.integer += ctx->draw_state.drawing_box.origin.y;
x += ctx->draw_state.drawing_box.origin.x;
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
graphics_private_draw_vertical_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, x, y1,
y2, ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
return;
}
#endif // PBL_COLOR
// since y1 is beginning of the line, rounding should work in favor of flooring the value
// therefore we substract one from the rounding addition to produce result similar to y2
int16_t y1_rounded =
(y1.raw_value + (FIXED_S16_3_ONE.raw_value / 2 - 1)) / FIXED_S16_3_ONE.raw_value;
int16_t y2_rounded = (y2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;
if (y1_rounded > y2_rounded) {
// AA algorithm will draw lines in one way only, so non-AA should reject those too
return;
}
prv_assign_line_vertical_non_aa(ctx, x, y1_rounded, y2_rounded);
}
void graphics_private_plot_pixel(GBitmap *framebuffer, GRect *clip_box, int x, int y,
uint16_t opacity, GColor color) {
// Plots pixel directly to framebuffer
// Pixel position have to be adjusted to drawing_box before calling this!
// Checking for clip box
const GPoint point = GPoint(x, y);
if (!grect_contains_point(clip_box, &point)) {
return;
}
#if PBL_COLOR
// Checking for data row min/max x
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, y);
if (!WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
return;
}
GColor *output = (GColor *)(data_row_info.data + x);
color.a = (uint8_t)(MAX_PLOT_BRIGHTNESS - opacity);
output->argb = gcolor_alpha_blend(color, (*output)).argb;
#else
if (opacity <= (MAX_PLOT_BRIGHTNESS / 2)) {
bool black = (gcolor_equal(color, GColorBlack));
uint8_t *line = ((uint8_t *)framebuffer->addr) + (framebuffer->row_size_bytes * y);
bitset8_update(line, x, !black);
}
#endif // PBL_COLOR
}
void graphics_private_plot_horizontal_line_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int y, int x0, int x1,
uint16_t opacity, GColor color) {
// Plots pixel directly to framebuffer
// Pixel position have to be adjusted to drawing_box before calling this!
// Checking for clip_box
if (!WITHIN(y, clip_box->origin.y, grect_get_max_y(clip_box) - 1)) {
return;
}
const int16_t x_min = MAX(MIN(x0, x1), clip_box->origin.x);
const int16_t x_max = MIN(MAX(x0, x1), grect_get_max_x(clip_box));
#if PBL_COLOR
color.a = (uint8_t)(MAX_PLOT_BRIGHTNESS - opacity);
#else
if (opacity > (MAX_PLOT_BRIGHTNESS / 2)) {
// We're not plotting anything, bail
return;
}
#endif // PBL_COLOR
ctx->draw_state.draw_implementation->blend_horizontal_line(ctx, y, x_min, x_max, color);
}
void graphics_private_plot_vertical_line_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int x, int y0, int y1,
uint16_t opacity, GColor color) {
// Plots pixel directly to framebuffer
// Pixel position have to be adjusted to drawing_box before calling this!
// Checking for clip_box
if (!WITHIN(x, clip_box->origin.x, grect_get_max_x(clip_box) - 1)) {
return;
}
int16_t y_min = MAX(MIN(y0, y1), clip_box->origin.y);
int16_t y_max = MIN(MAX(y0, y1), clip_box->origin.y + clip_box->size.h);
#if PBL_COLOR
color.a = (uint8_t)(MAX_PLOT_BRIGHTNESS - opacity);
#else
if (opacity > (MAX_PLOT_BRIGHTNESS / 2)) {
// We're not plotting anything, bail
return;
}
#endif // PBL_COLOR
ctx->draw_state.draw_implementation->blend_vertical_line(ctx, x, y_min, y_max, color);
}
void graphics_private_plot_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1, Fixed_S16_3 x2,
uint16_t opacity) {
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
// apply draw box and clipping
x1.integer += ctx->draw_state.drawing_box.origin.x;
x2.integer += ctx->draw_state.drawing_box.origin.x;
y += ctx->draw_state.drawing_box.origin.y;
// round edges:
x1.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
x2.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
if (x2.fraction > (opacity << 1)) {
x2.integer++;
}
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
graphics_private_plot_horizontal_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, y,
x1.integer, x2.integer, opacity,
ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
return;
}
#endif // PBL_COLOR
if (opacity <= (MAX_PLOT_BRIGHTNESS / 2)) {
int16_t x1_rounded = (x1.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
/ FIXED_S16_3_ONE.raw_value;
int16_t x2_rounded = (x2.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
/ FIXED_S16_3_ONE.raw_value;
prv_assign_line_horizontal_non_aa(ctx, y, x1_rounded, x2_rounded);
}
}
void graphics_private_plot_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1, Fixed_S16_3 y2,
uint16_t opacity) {
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
// apply draw box and clipping
x += ctx->draw_state.drawing_box.origin.x;
y1.integer += ctx->draw_state.drawing_box.origin.y;
y2.integer += ctx->draw_state.drawing_box.origin.y;
// round edges:
y1.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
y2.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
if (y2.fraction > (opacity << 1)) {
y2.integer++;
}
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
graphics_private_plot_vertical_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, x,
y1.integer, y2.integer, opacity,
ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
return;
}
#endif // PBL_COLOR
if (opacity <= (MAX_PLOT_BRIGHTNESS / 2)) {
int16_t y1_rounded = (y1.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
/ FIXED_S16_3_ONE.raw_value;
int16_t y2_rounded = (y2.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
/ FIXED_S16_3_ONE.raw_value;
prv_assign_line_vertical_non_aa(ctx, x, y1_rounded, y2_rounded);
}
}
#if PBL_COLOR
void graphics_private_draw_horizontal_line_delta_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int16_t y,
Fixed_S16_3 x1, Fixed_S16_3 x2,
Fixed_S16_3 delta1, Fixed_S16_3 delta2,
GColor color) {
// Extended sides AA calculations
uint8_t left_aa_offset = (delta1.integer > 1) ?
((delta1.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value) : 1;
uint8_t right_aa_offset = (delta2.integer > 1) ?
((delta2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value) : 1;
x1.integer -= left_aa_offset / 2;
x2.integer -= right_aa_offset / 2;
// look for clipbox
if (!WITHIN(y, clip_box->origin.y, grect_get_max_y(clip_box) - 1)) {
return;
}
const int16_t min_valid_x = clip_box->origin.x;
const int16_t max_valid_x = grect_get_max_x(clip_box) - 1;
// x1/x2 clipping and verification happens in raw drawing function to preserve gradients
ctx->draw_state.draw_implementation->assign_horizontal_line_delta(ctx, y, x1, x2,
left_aa_offset, right_aa_offset,
min_valid_x, max_valid_x,
color);
}
void graphics_private_draw_horizontal_line_delta_aa(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, Fixed_S16_3 delta1,
Fixed_S16_3 delta2) {
// apply draw box and clipping
x1.integer += ctx->draw_state.drawing_box.origin.x;
x2.integer += ctx->draw_state.drawing_box.origin.x;
y += ctx->draw_state.drawing_box.origin.y;
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
if (!framebuffer) {
// Couldn't capture framebuffer
return;
}
graphics_private_draw_horizontal_line_delta_prepared(ctx, framebuffer, &ctx->draw_state.clip_box,
y, x1, x2, delta1, delta2,
ctx->draw_state.stroke_color);
graphics_release_frame_buffer(ctx, framebuffer);
}
#endif // PBL_COLOR
void graphics_private_draw_horizontal_line_delta_non_aa(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, Fixed_S16_3 delta1,
Fixed_S16_3 delta2) {
int16_t x1_rounded = (x1.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;
int16_t x2_rounded = (x2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;
if (x1_rounded > x2_rounded) {
// AA algorithm will draw lines in one way only, so non-AA should reject those too
return;
}
prv_assign_line_horizontal_non_aa(ctx, y, x1_rounded, x2_rounded);
}
// This function will replicate source column in given area
T_STATIC void prv_replicate_column_row_raw(GBitmap *framebuffer, int16_t src_x, int16_t dst_x1,
int16_t dst_x2) {
const GRect column_to_replicate = (GRect) {
.origin = GPoint(src_x, framebuffer->bounds.origin.y),
.size = GSize(1, framebuffer->bounds.size.h),
};
GBitmap column_to_replicate_sub_bitmap;
gbitmap_init_as_sub_bitmap(&column_to_replicate_sub_bitmap, framebuffer, column_to_replicate);
for (int16_t x = dst_x1; x <= dst_x2; x++) {
bitblt_bitmap_into_bitmap(framebuffer, &column_to_replicate_sub_bitmap, GPoint(x, 0),
GCompOpAssign, GColorWhite);
}
}
void graphics_patch_trace_of_moving_rect(GContext *ctx, int16_t *prev_x, GRect current) {
const int16_t new_x = current.origin.x;
int16_t src_x = 0; // just so that GCC accepts that src_x is always initialized
int16_t dst_x1 = INT16_MAX;
int16_t dst_x2 = INT16_MIN;
if (*prev_x == INT16_MAX) {
// do nothing
} else if (*prev_x > new_x) {
// move to left
src_x = new_x + current.size.w - 1;
dst_x1 = src_x + 1;
dst_x2 = DISP_COLS - 1;
} else if (*prev_x < new_x) {
src_x = new_x;
dst_x1 = 0;
dst_x2 = src_x - 1;
}
*prev_x = new_x;
if (dst_x1 > dst_x2) {
return;
}
GBitmap *fb = graphics_capture_frame_buffer(ctx);
if (!fb) {
return;
}
prv_replicate_column_row_raw(fb, src_x, dst_x1, dst_x2);
graphics_release_frame_buffer(ctx, fb);
}
void graphics_private_move_pixels_horizontally(GBitmap *bitmap, int16_t delta_x,
bool patch_garbage) {
if (!bitmap || delta_x == 0) {
return;
}
const int bpp = gbitmap_get_bits_per_pixel(bitmap->info.format);
const int16_t abs_delta = ABS(delta_x);
const bool delta_neg = (delta_x < 0);
const int16_t min_y = bitmap->bounds.origin.y;
const int16_t max_y = grect_get_max_y(&bitmap->bounds) - 1;
for (int16_t y = min_y; y <= max_y; y++) {
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, y);
const int16_t min_x = MAX(row_info.min_x, bitmap->bounds.origin.x);
const int16_t max_x = MIN(row_info.max_x, grect_get_max_x(&bitmap->bounds) - 1);
const int16_t num_pix_data_row = max_x - min_x + 1;
const int16_t pixels_to_move = num_pix_data_row - abs_delta;
switch (bpp) {
case 1: {
// Note: this doesn't care about the bounding, because we don't have any round 1bpp
// devices to support, and it simplifies the code.
#if PBL_ROUND
WTF;
#endif
uint8_t *const buf = row_info.data;
const int delta_bytes = abs_delta / 8;
const int delta_bits = abs_delta % 8;
// Subtract two bytes to account for the 16-bit padding at the end of each row
const int bytes = bitmap->row_size_bytes - 2;
uint8_t *const left_pixel = buf;
uint8_t *const right_pixel = buf + delta_bytes;
const uint8_t fill_byte = (delta_neg ? (buf[bytes - 1] & 0x80) : buf[0] & 1) ? 0xFF : 0;
if (pixels_to_move <= 0) {
// on this row, the delta is wider than the available pixels
if (patch_garbage) {
memset(left_pixel, fill_byte, bytes);
}
break;
}
uint8_t *const from = delta_neg ? right_pixel : left_pixel;
uint8_t *const to = delta_neg ? left_pixel : right_pixel;
uint8_t *const garbage_start = delta_neg ? left_pixel + bytes - delta_bytes : left_pixel;
if (delta_bytes) {
memmove(to, from, bytes - delta_bytes);
if (patch_garbage) {
memset(garbage_start, fill_byte, delta_bytes);
}
}
if (delta_neg) {
if (delta_bits) {
const int rshift = delta_bits;
const int lshift = 8 - rshift;
for (int i = 0; i < bytes - 1; i++) {
buf[i] = (buf[i] >> rshift) | (buf[i+1] << lshift);
}
if (patch_garbage) {
buf[bytes - 1] >>= rshift;
buf[bytes - 1] |= fill_byte << lshift;
} else {
// Leave shifted-out areas alone
buf[bytes - 1] = (buf[bytes - 1] >> rshift) | (buf[bytes - 1] & (0xFF << lshift));
}
}
} else {
if (delta_bits) {
const int lshift = delta_bits;
const int rshift = 8 - lshift;
for (int i = bytes - 1; i >= 1; i--) {
buf[i] = (buf[i] << lshift) | (buf[i-1] >> rshift);
}
if (patch_garbage) {
buf[0] <<= lshift;
buf[0] |= fill_byte >> rshift;
} else {
// Leave shifted-out areas alone
buf[0] = (buf[0] << lshift) | (buf[0] & (0xFF >> rshift));
}
}
}
break;
}
case 8: {
uint8_t *const left_pixel = row_info.data + min_x;
uint8_t *const right_pixel = left_pixel + abs_delta;
if (pixels_to_move <= 0) {
// on this row, the delta is wider than the available pixels
if (patch_garbage) {
const uint8_t fill_byte = delta_neg ? left_pixel[num_pix_data_row - 1] :
left_pixel[0];
memset(left_pixel, fill_byte, num_pix_data_row);
}
break;
}
uint8_t *const from = delta_neg ? right_pixel : left_pixel;
uint8_t *const to = delta_neg ? left_pixel : right_pixel;
uint8_t *const garbage_start = delta_neg ? left_pixel + pixels_to_move : left_pixel;
const uint8_t fill_byte = delta_neg ? right_pixel[pixels_to_move - 1] : left_pixel[0];
memmove(to, from, (size_t)pixels_to_move);
if (patch_garbage) {
memset(garbage_start, fill_byte, abs_delta);
}
break;
}
default:
WTF;
}
}
}
void graphics_private_move_pixels_vertically(GBitmap *bitmap, int16_t delta_y) {
if (!bitmap || (delta_y == 0)) {
return;
}
const int bpp = gbitmap_get_bits_per_pixel(bitmap->info.format);
const bool delta_neg = (delta_y < 0);
const int16_t abs_delta = ABS(delta_y);
const int16_t min_y = bitmap->bounds.origin.y;
const int16_t max_y = grect_get_max_y(&bitmap->bounds) - 1;
const int16_t max_x = grect_get_max_x(&bitmap->bounds) - 1;
const int16_t iterate_dir = delta_neg ? -1 : 1;
const int16_t end_y = delta_neg ? max_y : min_y;
const int16_t start_y = delta_neg ? min_y + abs_delta : max_y - abs_delta;
if ((!delta_neg && (start_y < end_y)) || (delta_neg && (start_y > end_y))) {
return;
}
for (int16_t y = start_y; y != end_y; y -= iterate_dir) {
const GBitmapDataRowInfo dst_row_info = gbitmap_get_data_row_info(bitmap, y + delta_y);
const GBitmapDataRowInfo src_row_info = gbitmap_get_data_row_info(bitmap, y);
switch (bpp) {
case 1: {
// Note: this doesn't care about the bounding, because we don't have any round 1bpp
// devices to support, and it simplifies the code.
#if PBL_ROUND
WTF;
#endif
memmove(dst_row_info.data, src_row_info.data, bitmap->row_size_bytes);
break;
}
case 8: {
const int16_t dst_min_x = MAX(dst_row_info.min_x, bitmap->bounds.origin.x);
const int16_t dst_max_x = MIN(dst_row_info.max_x, max_x);
const int16_t dst_pixels = dst_max_x - dst_min_x + 1;
const int16_t src_min_x = MAX(src_row_info.min_x, bitmap->bounds.origin.x);
const int16_t src_max_x = MIN(src_row_info.max_x, max_x);
const int16_t src_pixels = src_max_x - src_min_x + 1;
const int16_t x_offset = src_min_x - dst_min_x;
const int16_t copy_pixels = MIN(src_pixels, dst_pixels);
memmove(dst_row_info.data + dst_min_x + x_offset, src_row_info.data + src_min_x,
(size_t)copy_pixels);
break;
}
default:
WTF;
}
}
}
GColor graphics_private_sample_line_color(const GBitmap *bitmap, GColorSampleEdge edge,
GColor fallback) {
if (!bitmap) {
return fallback;
}
GColor color = fallback;
const int bpp = gbitmap_get_bits_per_pixel(bitmap->info.format);
const int16_t min_x = bitmap->bounds.origin.x;
const int16_t min_y = bitmap->bounds.origin.y;
const int16_t end_x = grect_get_max_x(&bitmap->bounds);
const int16_t end_y = grect_get_max_y(&bitmap->bounds);
const bool horiz_advance = (edge == GColorSampleEdgeUp) || (edge == GColorSampleEdgeDown);
const bool edge_is_max_position = (edge == GColorSampleEdgeDown) ||
(edge == GColorSampleEdgeRight);
const int16_t length = horiz_advance ? (end_x - min_x) : (end_y - min_y);
for (int16_t i = 0; i < length; i++) {
const int16_t x = horiz_advance ? min_x + i : (edge_is_max_position ? end_x - 1 : min_x);
const int16_t y = !horiz_advance ? min_y + i : (edge_is_max_position ? end_y - 1 : min_y);
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(bitmap, y);
GColor this_color;
switch (bpp) {
case 1:
this_color = (data_row_info.data[x / 8] & (0x1 << (x % 8))) ? GColorWhite : GColorBlack;
break;
case 8:
this_color.argb = data_row_info.data[x];
break;
default:
WTF;
}
if (i == 0) {
color.argb = this_color.argb;
} else if (color.argb != this_color.argb) {
return fallback;
}
}
return color;
}

View file

@ -0,0 +1,167 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
#define MAX_PLOT_BRIGHTNESS 3
#define MAX_PLOT_OPACITY 0
#define MAX_RADIUS_LOOKUP 13
//! Plots pixel at given coordinates
//! Note this does not adjust to drawing_box!
//! @internal
//! @param ctx Graphics context for drawing
//! @param point Point to set pixel at using draw state's stroke color
void graphics_private_set_pixel(GContext* ctx, GPoint point);
//! Draws horizontal line with antialiased starting and ending pixel
//! Will adjust to the drawing_box and clip_box
//! Note: this only works for lines where x1 < x2
//! @param ctx Graphics context for drawing
//! @param y Integral Y coordinate for line
//! @param x1 Fixedpoint X coordinate for starting point
//! @param x2 Fixedpoint X coordinate for ending point
//! @internal
void graphics_private_draw_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2);
//! Draws horizontal line into framebuffer, requires adjustment for drawing_box and clip_box
//! @param ctx Graphics context for drawing
//! @param y Integral Y coordinate for line
//! @param x1 Integral X coordinate for starting point
//! @param x2 Integral X coordinate for ending point
//! @param color Color to be used
//! @internal
void graphics_private_draw_horizontal_line_integral(GContext *ctx, GBitmap *framebuffer, int16_t y,
int16_t x1, int16_t x2, GColor color);
//! Draws vertical line with antialiased starting and ending pixel
//! Will adjust to the drawing_box and clip_box
//! Note: this only works for lines where y1 < y2
//! @param ctx Graphics context for drawing
//! @param x Integral X coordinate for line
//! @param y1 Fixedpoint Y coordinate for starting point
//! @param y2 Fixedpoint Y coordinate for ending point
//! @internal
void graphics_private_draw_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1, Fixed_S16_3 y2);
//! Draws horizontal line with antialiased starting and ending pixel
//! Will use clip_box for clipping
//! Note: this does not adjust for drawing_box
//! Note: this only works for lines where x1 < x2
//! @param ctx Graphics context for drawing
//! @param y Integral Y coordinate for line
//! @param x1 Fixedpoint X coordinate for starting point
//! @param x2 Fixedpoint X coordinate for ending point
//! @internal
void graphics_private_draw_horizontal_line_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, GColor color);
//! Draws vertical line with antialiased starting and ending pixel
//! Will use clip_box for clipping
//! Note: this does not adjust for drawing_box
//! Note: this only works for lines where y1 < y2
//! @param ctx Graphics context for drawing
//! @param x Integral X coordinate for line
//! @param y1 Fixedpoint Y coordinate for starting point
//! @param y2 Fixedpoint Y coordinate for ending point
//! @internal
void graphics_private_draw_vertical_line_prepared(GContext *ctx, GBitmap *framebuffer,
GRect *clip_box, int16_t x, Fixed_S16_3 y1,
Fixed_S16_3 y2, GColor color);
//! Blends pixel at given coordinates into given bitmap (framebuffer)
//! Will use given clip_box for clipping
//! Note: this will not adjust for drawing_box
//! @param ctx Graphics context for plotting
//! @param framebuffer Address of framebuffer to plot pixel into
//! @param clip_box Address of clipping rectangle to perform clipping check
//! @param x Integral X coordinate of the point
//! @param y Integral Y coordinate of the point
//! @param opacity Value that will be reverted and applied to alpha channel
//! @param color Color of the pixel to blend
//! @internal
void graphics_private_plot_pixel(GBitmap *framebuffer, GRect *clip_box, int x, int y,
uint16_t opacity, GColor color);
//! Blends horizontal line between given points using current stroke color
//! Will adjust to drawing_box and clip_box
//! @param ctx Graphics context for plotting
//! @param y Y coordinate of line
//! @param x1 Starting point for the line
//! @param x2 Ending point for the line
//! @param opacity Value that will be reverted and applied to alpha channel
//! if off this will just revert to regular line with full opacity
void graphics_private_plot_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1, Fixed_S16_3 x2,
uint16_t opacity);
//! Blends vertical line between given points using current stroke color
//! Will adjust to drawing_box and clip_box
//! @param ctx Graphics context for plotting
//! @param x X coordinate of line
//! @param y1 Starting point for the line
//! @param y2 Ending point for the line
//! @param opacity Value that will be reverted and applied to alpha channel
//! if off this will just revert to regular line with full opacity
void graphics_private_plot_vertical_line(GContext *ctx, int16_t y, Fixed_S16_3 y1, Fixed_S16_3 y2,
uint16_t opacity);
//! Blending of vertical line used in gpath filling algorithm
void graphics_private_draw_horizontal_line_delta_aa(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, Fixed_S16_3 delta1,
Fixed_S16_3 delta2);
//! duplicates the outer-most pixel from a current rectangle to fill a GContext as if that
//! rectangle moved from prev_x to current.origin.x
//! will update prev_x afterwards
void graphics_patch_trace_of_moving_rect(GContext *ctx, int16_t *prev_x, GRect current);
//! will move all pixels in the bitmap by delta_x.
//! @param delta_x Number of pixels to move. Positive is right, negative is left.
//! @param patch_garbage If set, will fill the undefined pixels with the edge-most color.
void graphics_private_move_pixels_horizontally(GBitmap *bitmap, int16_t delta_x,
bool patch_garbage);
//! will move all pixels in the bitmap by delta_y - they will leave a trace of undefined pixels
//! @param delta_y Number of pixels to move. Positive is down, negative is up.
void graphics_private_move_pixels_vertically(GBitmap *bitmap, int16_t delta_y);
//! Returns grayscale pattern
//! @internal
//! @param color Input color
//! @param row_number Absolute number of framebuffer row
uint32_t graphics_private_get_1bit_grayscale_pattern(GColor color, uint8_t row_number);
//! Which edge of the bitmap to sample. This is identical in order to
//! CompositorTransitionDirection, and both should be wrapped into one enum as described in
//! PBL-40961
typedef enum {
GColorSampleEdgeUp,
GColorSampleEdgeDown,
GColorSampleEdgeLeft,
GColorSampleEdgeRight,
} GColorSampleEdge;
//! Samples a line of colors for a bitmap, then returns the color it found. If it found more than
//! one color or did not sample any pixels, it will return `fallback`.
//! @internal
//! @param bitmap Bitmap to sample from
//! @param edge Which edge of the bitmap to sample
//! @param fallback The color to return if no pixels were sampled or the line was not colored
//! homogeneously
GColor graphics_private_sample_line_color(const GBitmap *bitmap, GColorSampleEdge edge,
GColor fallback);

View file

@ -0,0 +1,339 @@
/*
* 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 "bitblt_private.h"
#include "graphics.h"
#include "graphics_private.h"
#include "graphics_private_raw.h"
#include "graphics_private_raw_mask.h"
#include "gtypes.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/graphics.h"
#include "util/math.h"
ALWAYS_INLINE void graphics_private_raw_blend_color_factor(const GContext *ctx, GColor *dst_color,
unsigned int data_offset,
GColor src_color, int x,
uint8_t factor) {
#if SCREEN_COLOR_DEPTH_BITS == 8
src_color.a = (uint8_t)(factor * 3 / (FIXED_S16_3_ONE.raw_value - 1));
const GColor blended_color = gcolor_alpha_blend(src_color, *dst_color);
#if CAPABILITY_HAS_MASKING
const GDrawMask *mask = ctx->draw_state.draw_mask;
graphics_private_raw_mask_apply(dst_color, mask, data_offset, x, 1, blended_color);
#else
*dst_color = blended_color;
#endif // CAPABILITY_HAS_MASKING
#endif // (SCREEN_COLOR_DEPTH_BITS == 8)
}
static ALWAYS_INLINE void prv_set_color(const GContext *ctx, GColor *dst_color,
unsigned int data_row_offset, int x, int width,
GColor src_color) {
#if CAPABILITY_HAS_MASKING
const GDrawMask *mask = ctx->draw_state.draw_mask;
graphics_private_raw_mask_apply(dst_color, mask, data_row_offset, x, width, src_color);
#else
memset(dst_color, src_color.argb, (size_t)width);
#endif // CAPABILITY_HAS_MASKING
}
// Plots row at given starting position and width, dithers grayscale colors
static void prv_assign_row_with_pattern_1bit(GBitmap *framebuffer, int16_t y, int16_t x,
int32_t width, GColor color) {
const uint32_t pattern = graphics_private_get_1bit_grayscale_pattern(color, (uint8_t) y);
uint32_t left_edge_block, right_edge_block, mask;
const uint32_t left_edge_bits_count = x % 32;
const uint32_t right_edge_bits_count = (x + width) % 32;
uint32_t *block = ((uint32_t*)framebuffer->addr) + (y * (framebuffer->row_size_bytes / 4))
+ (x / 32);
bool both_edges_in_same_block = (left_edge_bits_count + width) < 32;
if (both_edges_in_same_block) {
left_edge_block = (0xffffffff << left_edge_bits_count);
right_edge_block = right_edge_bits_count ? (0xffffffff >> (32 - right_edge_bits_count)) : 0;
mask = (left_edge_block & right_edge_block);
*(block) = (*(block) & ~mask) | (pattern & mask);
} else {
if (left_edge_bits_count) {
mask = 0xffffffff << left_edge_bits_count;
*(block) = (*(block) & ~mask) | (pattern & mask);
block++;
width -= (32 - left_edge_bits_count);
}
if (right_edge_bits_count) {
mask = 0xffffffff >> (32 - right_edge_bits_count);
*(block + (width / 32)) = (*(block + (width / 32)) & ~mask) | (pattern & mask);
width -= right_edge_bits_count;
}
if (width > 0) {
memset(block, pattern, (width / 8));
}
}
}
// ## Line blending functions:
// This function draws horizontal line with AA edges, given values have to be adjusted for
// screen coordinates and clipped according to the clip box, does not respect transparency
// on the drawn line (beside edges)
T_STATIC void prv_assign_horizontal_line_raw(GContext *ctx, int16_t y, Fixed_S16_3 x1,
Fixed_S16_3 x2, GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
PBL_ASSERTN(framebuffer->bounds.origin.x == 0 && framebuffer->bounds.origin.y == 0);
// Clip the line to the bitmap data row's range
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, y);
x1.raw_value = MAX(x1.raw_value, data_row_info.min_x << FIXED_S16_3_PRECISION);
x2.raw_value = MIN(x2.raw_value, data_row_info.max_x << FIXED_S16_3_PRECISION);
if (x1.integer > x2.integer) {
return;
}
#if PBL_COLOR
GColor8 *output = (GColor8 *)(data_row_info.data + x1.integer);
// first pixel with blending if fraction is different than 0
const unsigned int data_row_offset = data_row_info.data - (uint8_t *)framebuffer->addr;
if (x1.fraction != 0) {
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x1.integer,
(uint8_t)(FIXED_S16_3_ONE.raw_value - x1.fraction));
output++;
x1.integer++;
}
// middle pixels
const int16_t width = x2.integer - x1.integer + 1;
if (width > 0) {
prv_set_color(ctx, output, data_row_offset, x1.integer, width, color);
output += width;
// x1 doesn't need to be increased as it's not used anymore in this function
}
// last pixel with blending (don't render first *and* last pixel if line length is 1)
if (x2.fraction != 0) {
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x2.integer,
(uint8_t)x2.fraction);
}
#else
// TODO: as part of PBL-30849 make this a first-class function
// also see prv_blend_horizontal_line_raw
const int16_t x1_rounded = (x1.raw_value + FIXED_S16_3_HALF.raw_value) / FIXED_S16_3_FACTOR;
const int16_t x2_rounded = (x2.raw_value + FIXED_S16_3_HALF.raw_value) / FIXED_S16_3_FACTOR;
prv_assign_row_with_pattern_1bit(framebuffer, y, x1_rounded, x2_rounded - x1_rounded + 1, color);
#endif
}
// This function draws vertical line with AA edges, given values have to be adjusted for
// screen coordinates and clipped according to the clip box, does not respect transparency
// on the drawn line (beside edges)
T_STATIC void prv_assign_vertical_line_raw(GContext *ctx, int16_t x, Fixed_S16_3 y1,
Fixed_S16_3 y2, GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
PBL_ASSERTN(framebuffer->bounds.origin.x == 0 && framebuffer->bounds.origin.y == 0);
GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, y1.integer);
GColor8 *output = (GColor8 *)(data_row_info.data + x);
// first pixel with blending
const unsigned int data_row_offset = data_row_info.data - (uint8_t *)framebuffer->addr;
if (y1.fraction != 0) {
// Only draw the pixel if its within the bitmap data row range
if (WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x,
(uint8_t)(FIXED_S16_3_ONE.raw_value - y1.fraction));
}
y1.integer++;
data_row_info = gbitmap_get_data_row_info(framebuffer, y1.integer);
output = (GColor8 *)(data_row_info.data + x);
}
// middle pixels
while (y1.integer <= y2.integer) {
// Only draw the pixel if its within the bitmap data row range
if (WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
prv_set_color(ctx, output, data_row_offset, x, 1, color);
}
y1.integer++;
data_row_info = gbitmap_get_data_row_info(framebuffer, y1.integer);
output = (GColor8 *)(data_row_info.data + x);
}
// last pixel with blending (don't render first *and* last pixel if line length is 1)
if (y2.fraction != 0) {
// Only draw the pixel if its within the bitmap data row range
if (WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x,
(uint8_t)y2.fraction);
}
}
}
// This function draws horizontal line with blending, given values have to be clipped and adjusted
// clip_box and draw_box respecively.
T_STATIC void prv_blend_horizontal_line_raw(GContext *ctx, int16_t y, int16_t x1, int16_t x2,
GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
// Clip the line to the bitmap data row's range
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, y);
x1 = MAX(x1, data_row_info.min_x);
x2 = MIN(x2, data_row_info.max_x);
#if PBL_COLOR
for (int i = x1; i <= x2; i++) {
GColor *output = (GColor *)(data_row_info.data + i);
const unsigned int data_row_offset = data_row_info.data - (uint8_t *)framebuffer->addr;
prv_set_color(ctx, output, data_row_offset, i, 1, gcolor_alpha_blend(color, *output));
}
#else
// TODO: as part of PBL-30849 make this a first-class function
// also see, prv_assign_horizontal_line_raw
prv_assign_row_with_pattern_1bit(framebuffer, y, x1, x2 - x1 + 1, color);
#endif // SCREEN_COLOR_DEPTH_BITS == 8
}
// This function draws vertical line with blending, given values have to be clipped and adjusted
// clip_box and draw_box respecively.
T_STATIC void prv_blend_vertical_line_raw(GContext *ctx, int16_t x, int16_t y1, int16_t y2,
GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
#if SCREEN_COLOR_DEPTH_BITS == 8
for (int i = y1; i < y2; i++) {
// Skip over pixels outside the bitmap data row's range
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, i);
if (!WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
continue;
}
GColor *output = (GColor *)(data_row_info.data + x);
const unsigned int data_row_offset = data_row_info.data - (uint8_t *)framebuffer->addr;
prv_set_color(ctx, output, data_row_offset, x, 1, gcolor_alpha_blend(color, *output));
}
#else
bool black = (gcolor_equal(color, GColorBlack));
for (int i = y1; i < y2; i++) {
uint8_t *line = ((uint8_t *)framebuffer->addr) + (framebuffer->row_size_bytes * i);
bitset8_update(line, x, !black);
}
#endif // SCREEN_COLOR_DEPTH_BITS == 8
}
// This function will draw a horizontal line with two gradients on side representing AA edges
T_STATIC void prv_assign_horizontal_line_delta_raw(GContext *ctx, int16_t y,
Fixed_S16_3 x1, Fixed_S16_3 x2,
uint8_t left_aa_offset, uint8_t right_aa_offset,
int16_t clip_box_min_x, int16_t clip_box_max_x,
GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
PBL_ASSERTN(framebuffer->bounds.origin.x == 0 && framebuffer->bounds.origin.y == 0);
// Clip the clip box to the bitmap data row's range
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, y);
clip_box_min_x = MAX(clip_box_min_x, data_row_info.min_x);
clip_box_max_x = MIN(clip_box_max_x, data_row_info.max_x);
// If x1 is further outside the clip box than the left gradient width, we need to move x1 up
// to clip_box_min_x and proceed such that we don't draw the left gradient
int16_t x1_distance_outside_clip_box = clip_box_min_x - x1.integer;
if (x1_distance_outside_clip_box > left_aa_offset) {
left_aa_offset = 0;
x1.integer += x1_distance_outside_clip_box;
}
// Clip x2 to clip_box_max_x
x2.integer = MIN(clip_box_max_x, x2.integer);
// Return early if there's nothing to draw
if (x1.integer > x2.integer) {
return;
}
GColor8 *output = (GColor8 *)(data_row_info.data + x1.integer);
// first pixel with blending
const unsigned int data_row_offset = data_row_info.data - (uint8_t *)framebuffer->addr;
if (left_aa_offset == 1) {
// To prevent bleeding of left-hand AA below clip_box
if (x1.integer >= clip_box_min_x) {
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x1.integer,
(uint8_t)(FIXED_S16_3_ONE.raw_value - x1.fraction));
}
output++;
x1.integer++;
// or first AA gradient with blending
} else {
for (int i = 0; i < left_aa_offset; i++) {
// To preserve gradient with clipping:
if (x1.integer < clip_box_min_x) {
output++;
x1.integer++;
continue;
}
if (x1.integer > clip_box_max_x) {
break;
}
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x1.integer,
(uint8_t)(FIXED_S16_3_ONE.raw_value * i /
left_aa_offset));
output++;
x1.integer++;
}
}
// middle pixels
const int16_t width = x2.integer - x1.integer + 1;
if (width > 0) {
prv_set_color(ctx, output, data_row_offset, x1.integer, width, color);
output += width;
x1.integer += width;
}
// last pixel with blending (don't render first *and* last pixel if line length is 1)
if (right_aa_offset <= 1) {
if (x1.integer <= clip_box_max_x) {
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x1.integer,
(uint8_t)x2.fraction);
}
// or last AA gradient with blending
} else {
for (int i = 0; i < right_aa_offset; i++) {
if (x1.integer > clip_box_max_x) {
break;
}
graphics_private_raw_blend_color_factor(ctx, output, data_row_offset, color, x1.integer,
(uint8_t)(FIXED_S16_3_ONE.raw_value *
(right_aa_offset - i) / right_aa_offset));
output++;
x1.integer++;
}
}
}
// TODO: Platform switches could happen here, too
const GDrawRawImplementation g_default_draw_implementation = {
.assign_horizontal_line = prv_assign_horizontal_line_raw,
.assign_vertical_line = prv_assign_vertical_line_raw,
.blend_horizontal_line = prv_blend_horizontal_line_raw,
.blend_vertical_line = prv_blend_vertical_line_raw,
.assign_horizontal_line_delta = prv_assign_horizontal_line_delta_raw,
};

View file

@ -0,0 +1,26 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
extern const GDrawRawImplementation g_default_draw_implementation;
void graphics_private_raw_blend_color_factor(const GContext *ctx, GColor *dst_color,
unsigned int data_offset,
GColor src_color, int x,
uint8_t factor);

View file

@ -0,0 +1,349 @@
/*
* 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_private_raw_mask.h"
#include "bitblt_private.h"
#include "gcontext.h"
#include "system/passert.h"
#include "util/graphics.h"
#if CAPABILITY_HAS_MASKING
// Clip the provided fixed x values to the framebuffer's data row info values for the row described
// by y. Return true if clipped values are valid for the row, false otherwise.
static bool prv_clip_fixed_x_values_to_data_row_info(GContext *ctx, int16_t y, Fixed_S16_3 *x1,
Fixed_S16_3 *x2) {
const GBitmap *framebuffer = &ctx->dest_bitmap;
const GBitmapDataRowInfo current_data_row_info = gbitmap_get_data_row_info(framebuffer,
(uint16_t)y);
if (x1 && x2) {
x1->raw_value = MAX(x1->raw_value, current_data_row_info.min_x << FIXED_S16_3_PRECISION);
x2->raw_value = MIN(x2->raw_value, current_data_row_info.max_x << FIXED_S16_3_PRECISION);
return x1->integer <= x2->integer;
}
return false;
}
// Clip the provided x values to the values in the provided data row info.
// Return true if clipped values are valid for the row, false otherwise.
static bool prv_clip_x_values_to_data_row_info(const GBitmapDataRowInfo *data_row_info, int16_t *x1,
int16_t *x2) {
if (data_row_info && x1 && x2) {
const int16_t clipped_x1 = MAX(*x1, data_row_info->min_x);
*x1 = clipped_x1;
const int16_t clipped_x2 = MIN(*x2, data_row_info->max_x);
*x2 = clipped_x2;
return clipped_x1 <= clipped_x2;
}
return false;
}
static void prv_update_mask(GContext *ctx, int16_t y, int16_t min_x, int16_t max_x,
GColor color) {
PBL_ASSERTN(ctx);
if (gcolor_is_invisible(color)) {
return;
}
GDrawMask *mask = ctx->draw_state.draw_mask;
PBL_ASSERTN(mask);
const GBitmap *framebuffer = &ctx->dest_bitmap;
const GBitmapDataRowInfo current_data_row_info = gbitmap_get_data_row_info(framebuffer,
(uint16_t)y);
if (!prv_clip_x_values_to_data_row_info(&current_data_row_info, &min_x, &max_x)) {
return;
}
// Update the relevant mask row pixel values
for (int x = min_x; x <= max_x; x++) {
const GPoint p = GPoint(x, y);
// Calculate the new mask pixel value
const GColor8Component src_color_luminance = gcolor_get_luminance(color);
const uint8_t current_mask_value = graphics_private_raw_mask_get_value(ctx, mask, p);
const uint8_t new_pixel_value =
g_bitblt_private_blending_mask_lookup[(color.a << 4) |
(current_mask_value << 2) |
src_color_luminance];
graphics_private_raw_mask_set_value(ctx, mask, p, new_pixel_value);
}
}
static void prv_blend_color_and_update_mask(GContext *ctx, int16_t y, int16_t min_x, int16_t max_x,
GColor color, uint8_t factor) {
color.a = (GColor8Component)(factor * 3 / (FIXED_S16_3_ONE.raw_value - 1));
prv_update_mask(ctx, y, min_x, max_x, color);
}
T_STATIC void prv_mask_recording_assign_horizontal_line(GContext *ctx, int16_t y,
Fixed_S16_3 x1, Fixed_S16_3 x2,
GColor color) {
if (!prv_clip_fixed_x_values_to_data_row_info(ctx, y, &x1, &x2)) {
return;
}
// first pixel with blending if fraction is different than 0
if (x1.fraction != 0) {
prv_blend_color_and_update_mask(ctx, y, x1.integer, x1.integer, color,
(uint8_t)(FIXED_S16_3_ONE.raw_value - x1.fraction));
x1.integer++;
}
// middle pixels
int16_t last_pixel_x = x2.integer;
if (x1.integer < x2.integer + 1) {
prv_update_mask(ctx, y, x1.integer, x2.integer, color);
// increment the last_pixel since we had some middle pixels
last_pixel_x++;
// x1 doesn't need to be increased as it's not used anymore in this function
}
// last pixel with blending (don't render first *and* last pixel if line length is 1)
if (x2.fraction != 0) {
prv_blend_color_and_update_mask(ctx, y, last_pixel_x, last_pixel_x, color,
(uint8_t)x2.fraction);
}
}
T_STATIC void prv_mask_recording_assign_vertical_line(GContext *ctx, int16_t x,
Fixed_S16_3 y1, Fixed_S16_3 y2,
GColor color) {
// first pixel with blending
if (y1.fraction != 0) {
prv_blend_color_and_update_mask(ctx, y1.integer, x, x, color,
(uint8_t)(FIXED_S16_3_ONE.raw_value - y1.fraction));
y1.integer++;
}
// middle pixels
while (y1.integer <= y2.integer) {
prv_update_mask(ctx, y1.integer, x, x, color);
y1.integer++;
}
// last pixel with blending (don't render first *and* last pixel if line length is 1)
if (y2.fraction != 0) {
prv_blend_color_and_update_mask(ctx, y1.integer, x, x, color, (uint8_t)y2.fraction);
}
}
T_STATIC void prv_mask_recording_blend_horizontal_line_raw(GContext *ctx, int16_t y, int16_t x1,
int16_t x2, GColor color) {
prv_update_mask(ctx, y, x1, x2, color);
}
T_STATIC void prv_mask_recording_blend_vertical_line_raw(GContext *ctx, int16_t x, int16_t y1,
int16_t y2, GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
for (int16_t i = y1; i <= y2; i++) {
// Skip over pixels outside the bitmap data row's range
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, (uint16_t)i);
if (!WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
continue;
}
prv_update_mask(ctx, i, x, x, color);
}
}
T_STATIC void prv_mask_recording_assign_horizontal_line_delta_raw(GContext *ctx, int16_t y,
Fixed_S16_3 x1, Fixed_S16_3 x2,
uint8_t left_aa_offset,
uint8_t right_aa_offset,
int16_t clip_box_min_x,
int16_t clip_box_max_x,
GColor color) {
PBL_ASSERTN(ctx);
GBitmap *framebuffer = &ctx->dest_bitmap;
PBL_ASSERTN(framebuffer->bounds.origin.x == 0 && framebuffer->bounds.origin.y == 0);
// Clip the clip box to the bitmap data row's range
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, (uint16_t)y);
clip_box_min_x = MAX(clip_box_min_x, data_row_info.min_x);
clip_box_max_x = MIN(clip_box_max_x, data_row_info.max_x);
// If x1 is further outside the clip box than the left gradient width, we need to move x1 up
// to clip_box_min_x and proceed such that we don't draw the left gradient
int16_t x1_distance_outside_clip_box = clip_box_min_x - x1.integer;
if (x1_distance_outside_clip_box > left_aa_offset) {
left_aa_offset = 0;
x1.integer += x1_distance_outside_clip_box;
}
// Clip x2 to clip_box_max_x
x2.integer = MIN(clip_box_max_x, x2.integer);
// Return early if there's nothing to draw
if (x1.integer > x2.integer) {
return;
}
// first pixel with blending
if (left_aa_offset == 1) {
// To prevent bleeding of left-hand AA below clip_box
if (x1.integer >= clip_box_min_x) {
prv_blend_color_and_update_mask(ctx, y, x1.integer, x1.integer, color,
(uint8_t)(FIXED_S16_3_ONE.raw_value - x1.fraction));
}
x1.integer++;
// or first AA gradient with blending
} else {
for (int i = 0; i < left_aa_offset; i++) {
// To preserve gradient with clipping:
if (x1.integer < clip_box_min_x) {
x1.integer++;
continue;
}
if (x1.integer > clip_box_max_x) {
break;
}
prv_blend_color_and_update_mask(ctx, y, x1.integer, x1.integer, color,
(uint8_t)(FIXED_S16_3_ONE.raw_value * i / left_aa_offset));
x1.integer++;
}
}
// middle pixels
if (x1.integer < x2.integer + 1) {
prv_update_mask(ctx, y, x1.integer, x2.integer, color);
}
// last pixel with blending (don't render first *and* last pixel if line length is 1)
if (right_aa_offset <= 1) {
if (x1.integer <= clip_box_max_x) {
prv_blend_color_and_update_mask(ctx, y, x1.integer, x1.integer, color, (uint8_t)x2.fraction);
}
// or last AA gradient with blending
} else {
for (int i = 0; i < right_aa_offset; i++) {
if (x1.integer > clip_box_max_x) {
break;
}
prv_blend_color_and_update_mask(ctx, y, x1.integer, x1.integer, color,
(uint8_t)(FIXED_S16_3_ONE.raw_value * (right_aa_offset - i) /
right_aa_offset));
x1.integer++;
}
}
}
const GDrawRawImplementation g_mask_recording_draw_implementation = {
.assign_horizontal_line = prv_mask_recording_assign_horizontal_line,
.assign_vertical_line = prv_mask_recording_assign_vertical_line,
.blend_horizontal_line = prv_mask_recording_blend_horizontal_line_raw,
.blend_vertical_line = prv_mask_recording_blend_vertical_line_raw,
.assign_horizontal_line_delta = prv_mask_recording_assign_horizontal_line_delta_raw,
// If you ever experience a crash while recording/using a mask, then it's likely that you need to
// provide additional draw handlers here
};
// Lookup table to "multiply" two alpha values
// dst.a = multiplied_alpha[src.a][dst.a];
static const GColor8Component s_multiplied_alpha_lookup[4][4] = {
{0, 0, 0, 0},
{0, 0, 1, 1},
{0, 1, 1, 2},
{0, 1, 2, 3},
};
void graphics_private_raw_mask_apply(GColor8 *dst_color, const GDrawMask *mask,
unsigned int data_row_offset, int x, int width,
GColor8 src_color) {
if (!dst_color) {
return;
}
// If there's no mask, just set the color normally and return
if (!mask) {
memset(dst_color, src_color.argb, (size_t)width);
return;
}
const uint8_t pixels_per_byte = (uint8_t)GDRAWMASK_PIXELS_PER_BYTE;
const unsigned int mask_row_data_offset = data_row_offset / pixels_per_byte;
const uint8_t *mask_row_data = &(((uint8_t *)mask->pixel_mask_data)[mask_row_data_offset]);
// Use 0 for row_stride_bytes and y since we've already moved the pointer to the row of interest
const uint16_t row_stride_bytes = 0;
// We have to adjust x because mask_row_data_offset might not be on a Byte boundary
const unsigned int x_adjustment = data_row_offset % pixels_per_byte;
for (int current_x = x; current_x < x + width; current_x++) {
const uint8_t mask_pixel_value = raw_image_get_value_for_bitdepth(mask_row_data,
current_x + x_adjustment,
0 /* y */, row_stride_bytes,
GDRAWMASK_BITS_PER_PIXEL);
// Make a copy of src_color and multiply its alpha with the mask pixel value
GColor8 alpha_adjusted_src_color = src_color;
alpha_adjusted_src_color.a = s_multiplied_alpha_lookup[mask_pixel_value][src_color.a];
// Blend alpha_adjusted_src_color with dst_color to produce the final dst_color
dst_color->argb = gcolor_alpha_blend(alpha_adjusted_src_color, *dst_color).argb;
dst_color++;
}
}
ALWAYS_INLINE uint8_t graphics_private_raw_mask_get_value(const GContext *ctx,
const GDrawMask *mask, GPoint p) {
const GBitmap *framebuffer_bitmap = &ctx->dest_bitmap;
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer_bitmap, p.y);
// Calculate a pointer to the start of the row of interest in the pixel mask data
const unsigned int data_row_info_offset =
data_row_info.data - (uint8_t *)framebuffer_bitmap->addr;
const uint8_t pixels_per_byte = GDRAWMASK_PIXELS_PER_BYTE;
const unsigned int mask_row_data_offset = data_row_info_offset / pixels_per_byte;
const uint8_t *mask_row_data = &(((uint8_t *)mask->pixel_mask_data)[mask_row_data_offset]);
// Use 0 for row_stride_bytes and y since we've already moved the pointer to the row of interest
const uint16_t row_stride_bytes = 0;
const uint32_t fake_y = 0;
// We have to adjust x because mask_row_data_offset might not be on a Byte boundary
const int adjusted_x = p.x + (data_row_info_offset % pixels_per_byte);
return raw_image_get_value_for_bitdepth(mask_row_data, adjusted_x, fake_y, row_stride_bytes,
GDRAWMASK_BITS_PER_PIXEL);
}
ALWAYS_INLINE void graphics_private_raw_mask_set_value(const GContext *ctx, GDrawMask *mask,
GPoint p, uint8_t value) {
const GBitmap *framebuffer_bitmap = &ctx->dest_bitmap;
const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer_bitmap, p.y);
// Calculate a pointer to the start of the row of interest in the pixel mask data
const unsigned int data_row_info_offset =
data_row_info.data - (uint8_t *)framebuffer_bitmap->addr;
const uint8_t pixels_per_byte = GDRAWMASK_PIXELS_PER_BYTE;
const unsigned int mask_row_data_offset = data_row_info_offset / pixels_per_byte;
uint8_t *mask_row_data = &(((uint8_t *)mask->pixel_mask_data)[mask_row_data_offset]);
// Use 0 for row_stride_bytes and y since we've already moved the pointer to the row of interest
const uint16_t row_stride_bytes = 0;
const uint32_t fake_y = 0;
// We have to adjust x because mask_row_data_offset might not be on a Byte boundary
const int adjusted_x = p.x + (data_row_info_offset % pixels_per_byte);
raw_image_set_value_for_bitdepth(mask_row_data, (uint32_t)adjusted_x, fake_y, row_stride_bytes,
GDRAWMASK_BITS_PER_PIXEL, value);
}
#endif // CAPABILITY_HAS_MASKING

View file

@ -0,0 +1,30 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
extern const GDrawRawImplementation g_mask_recording_draw_implementation;
void graphics_private_raw_mask_apply(GColor8 *dst_color, const GDrawMask *mask,
unsigned int data_row_offset, int x, int width,
GColor8 src_color);
uint8_t graphics_private_raw_mask_get_value(const GContext *ctx, const GDrawMask *mask, GPoint p);
void graphics_private_raw_mask_set_value(const GContext *ctx, GDrawMask *mask, GPoint p,
uint8_t value);

View file

@ -0,0 +1,256 @@
/*
* 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 "gtypes.h"
#include "gtransform.h"
#include "util/trig.h"
#include <string.h>
//////////////////////////////////////
/// Creating Transforms
//////////////////////////////////////
// Note that int64_t casting is required since cos/sin values are already in a 32-bit fixed point
// representation and when passed to this function. They need to be scaled by the
// Fixed_S32_16 precision (16-bits) before dividing by the TRIG_MAX_RATIO. This multiply is what
// makes the int64_t casting necessary to avoid overflowing across 32-bits.
GTransform gtransform_init_rotation(int32_t angle) {
if (angle != 0) {
int32_t cosine = cos_lookup(angle);
int32_t sine = sin_lookup(angle);
int64_t cosine_val = (cosine * ((int64_t)GTransformNumberOne.raw_value)) / TRIG_MAX_RATIO;
int64_t sine_val = (sine * ((int64_t)GTransformNumberOne.raw_value)) / TRIG_MAX_RATIO;
GTransformNumber a = (GTransformNumber) { .raw_value = cosine_val };
GTransformNumber b = (GTransformNumber) { .raw_value = -sine_val };
GTransformNumber c = (GTransformNumber) { .raw_value = sine_val };
GTransformNumber d = (GTransformNumber) { .raw_value = cosine_val };
return GTransform(a, b, c, d, GTransformNumberZero, GTransformNumberZero);
} else {
return GTransformIdentity();
}
}
//////////////////////////////////////
/// Evaluating Transforms
//////////////////////////////////////
bool gtransform_is_identity(const GTransform * const t) {
if (!t) {
return false;
}
GTransform t_c = GTransformIdentity();
if (memcmp(t, &t_c, sizeof(GTransform)) == 0) {
return true;
}
return false;
}
bool gtransform_is_only_scale(const GTransform * const t) {
if (!t) {
return false;
}
if ((t->b.raw_value == GTransformNumberZero.raw_value) &&
(t->c.raw_value == GTransformNumberZero.raw_value) &&
(t->tx.raw_value == GTransformNumberZero.raw_value) &&
(t->ty.raw_value == GTransformNumberZero.raw_value)) {
return true;
}
return false;
}
bool gtransform_is_only_translation(const GTransform * const t) {
if (!t) {
return false;
}
if ((t->a.raw_value == GTransformNumberOne.raw_value) &&
(t->b.raw_value == GTransformNumberZero.raw_value) &&
(t->c.raw_value == GTransformNumberZero.raw_value) &&
(t->d.raw_value == GTransformNumberOne.raw_value)) {
return true;
}
return false;
}
bool gtransform_is_only_scale_or_translation(const GTransform * const t) {
if (!t) {
return false;
}
if ((t->b.raw_value != GTransformNumberZero.raw_value) ||
(t->c.raw_value != GTransformNumberZero.raw_value)) {
return true;
}
return false;
}
bool gtransform_is_equal(const GTransform * const t1, const GTransform * const t2) {
if ((!t1) || (!t2)) {
return false;
}
return memcmp(t1, t2, sizeof(GTransform)) == 0;
}
//////////////////////////////////////
/// Modifying Transforms
//////////////////////////////////////
// Note that t_new can be set to either of t1 or t2 safely to do in place muliplication
// Note this operation is not commutative. The operation is as follows t_new = t1 * t2
void gtransform_concat(GTransform *t_new, const GTransform *t1, const GTransform * t2) {
if ((!t_new) || (!t1) || (!t2)) {
return;
}
Fixed_S32_16 a_a = Fixed_S32_16_mul(t1->a, t2->a);
Fixed_S32_16 b_c = Fixed_S32_16_mul(t1->b, t2->c);
Fixed_S32_16 a_b = Fixed_S32_16_mul(t1->a, t2->b);
Fixed_S32_16 b_d = Fixed_S32_16_mul(t1->b, t2->d);
Fixed_S32_16 c_a = Fixed_S32_16_mul(t1->c, t2->a);
Fixed_S32_16 d_c = Fixed_S32_16_mul(t1->d, t2->c);
Fixed_S32_16 c_b = Fixed_S32_16_mul(t1->c, t2->b);
Fixed_S32_16 d_d = Fixed_S32_16_mul(t1->d, t2->d);
Fixed_S32_16 tx_a = Fixed_S32_16_mul(t1->tx, t2->a);
Fixed_S32_16 ty_c = Fixed_S32_16_mul(t1->ty, t2->c);
Fixed_S32_16 tx_b = Fixed_S32_16_mul(t1->tx, t2->b);
Fixed_S32_16 ty_d = Fixed_S32_16_mul(t1->ty, t2->d);
t_new->a = Fixed_S32_16_add(a_a, b_c);
t_new->b = Fixed_S32_16_add(a_b, b_d);
t_new->c = Fixed_S32_16_add(c_a, d_c);
t_new->d = Fixed_S32_16_add(c_b, d_d);
t_new->tx = Fixed_S32_16_add3(tx_a, ty_c, t2->tx);
t_new->ty = Fixed_S32_16_add3(tx_b, ty_d, t2->ty);
}
void gtransform_scale(GTransform *t_new, GTransform *t, GTransformNumber sx, GTransformNumber sy) {
if ((!t_new) || (!t)) {
return;
}
// Copy over t to t_new and update as necessary
if (t_new != t) {
memcpy(t_new, t, sizeof(GTransform));
}
// t_new = ts*t
// Scale X vector (a and b)
t_new->a = Fixed_S32_16_mul(sx, t->a);
t_new->b = Fixed_S32_16_mul(sx, t->b);
// Scale Y vector (c and d)
t_new->c = Fixed_S32_16_mul(sy, t->c);
t_new->d = Fixed_S32_16_mul(sy, t->d);
}
void gtransform_translate(GTransform *t_new, GTransform *t,
GTransformNumber tx, GTransformNumber ty) {
if ((!t_new) || (!t)) {
return;
}
// Copy over t to t_new and update as necessary
if (t_new != t) {
memcpy(t_new, t, sizeof(GTransform));
}
// t_new = tt*t
Fixed_S32_16 tx_a = Fixed_S32_16_mul(tx, t->a);
Fixed_S32_16 ty_c = Fixed_S32_16_mul(ty, t->c);
Fixed_S32_16 tx_b = Fixed_S32_16_mul(tx, t->b);
Fixed_S32_16 ty_d = Fixed_S32_16_mul(ty, t->d);
t_new->tx = Fixed_S32_16_add3(tx_a, ty_c, t->tx);
t_new->ty = Fixed_S32_16_add3(tx_b, ty_d, t->ty);
}
void gtransform_rotate(GTransform *t_new, GTransform *t, int32_t angle) {
if ((!t_new) || (!t)) {
return;
}
// t_new = tr*t
GTransform tR = gtransform_init_rotation(angle);
gtransform_concat(t_new, &tR, t);
}
bool gtransform_invert(GTransform *t_new, GTransform *t) {
if ((!t_new) || (!t)) {
return false;
}
memcpy(t_new, t, sizeof(GTransform));
// FIXME: NYI - copy original into t_new for now
return false;
}
//////////////////////////////////////
/// Applying Transformations
//////////////////////////////////////
GPointPrecise gpoint_transform(GPoint point, const GTransform * const t) {
GPointPrecise pointP = GPointPreciseFromGPoint(point);
if (!t) {
return pointP;
}
Fixed_S16_3 x_a = Fixed_S16_3_S32_16_mul(pointP.x, t->a);
Fixed_S16_3 y_c = Fixed_S16_3_S32_16_mul(pointP.y, t->c);
Fixed_S16_3 one_tx = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->tx);
Fixed_S16_3 x_b = Fixed_S16_3_S32_16_mul(pointP.x, t->b);
Fixed_S16_3 y_d = Fixed_S16_3_S32_16_mul(pointP.y, t->d);
Fixed_S16_3 one_ty = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->ty);
Fixed_S16_3 sum_x = Fixed_S16_3_add3(x_a, y_c, one_tx);
Fixed_S16_3 sum_y = Fixed_S16_3_add3(x_b, y_d, one_ty);
return GPointPrecise(sum_x.raw_value, sum_y.raw_value);
}
GVectorPrecise gvector_transform(GVector vector, const GTransform * const t) {
GVectorPrecise vectorP = GVectorPreciseFromGVector(vector);
if (!t) {
return vectorP;
}
Fixed_S16_3 x_a = Fixed_S16_3_S32_16_mul(vectorP.dx, t->a);
Fixed_S16_3 y_c = Fixed_S16_3_S32_16_mul(vectorP.dy, t->c);
Fixed_S16_3 one_tx = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->tx);
Fixed_S16_3 x_b = Fixed_S16_3_S32_16_mul(vectorP.dx, t->b);
Fixed_S16_3 y_d = Fixed_S16_3_S32_16_mul(vectorP.dy, t->d);
Fixed_S16_3 one_ty = Fixed_S16_3_S32_16_mul(FIXED_S16_3_ONE, t->ty);
Fixed_S16_3 sum_x = Fixed_S16_3_add3(x_a, y_c, one_tx);
Fixed_S16_3 sum_y = Fixed_S16_3_add3(x_b, y_d, one_ty);
return GVectorPrecise(sum_x.raw_value, sum_y.raw_value);
}

View file

@ -0,0 +1,263 @@
/*
* 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.
*/
#pragma once
#include "gtypes.h"
#include "util/math_fixed.h"
//! @addtogroup Graphics
//! @{
//! @addtogroup GraphicsTransforms Transformation Matrices
//! \brief Types for creating transformation matrices and utility functions to manipulate and apply
//! the transformations.
//!
//! @{
//////////////////////////////////////
/// Creating Transforms
//////////////////////////////////////
//! Convenience macro for GTransformNumber equal to 0
#define GTransformNumberZero ((GTransformNumber){ .integer = 0, .fraction = 0 })
//! Convenience macro for GTransformNumber equal to 1
#define GTransformNumberOne ((GTransformNumber){ .integer = 1, .fraction = 0 })
//! Convenience macro to convert from a number (i.e. char, int, float, etc.) to GTransformNumber
//! @param x The number to convert
#define GTransformNumberFromNumber(x) \
((GTransformNumber){ .raw_value = (int32_t)((x)*(GTransformNumberOne.raw_value)) })
//! This macro returns the transformation matrix for the corresponding input coefficients.
//! Below is the equivalent resulting matrix:
//! t = [ a b 0 ]
//! [ c d 0 ]
//! [ tx ty 1 ]
//! @param a Coefficient corresponding to X scale (type is GTransformNumber)
//! @param b Coefficient corresponding to X shear (type is GTransformNumber)
//! @param c Coefficient corresponding to Y shear (type is GTransformNumber)
//! @param d Coefficient corresponding to Y scale (type is GTransformNumber)
//! @param tx Coefficient corresponding to X translation (type is GTransformNumber)
//! @param ty Coefficient corresponding to Y translation (type is GTransformNumber)
#define GTransform(a, b, c, d, tx, ty) (GTransform) { (a), (b), (c), (d), (tx), (ty) }
//! @param a Coefficient corresponding to X scale (type is char, int, float, etc)
//! @param b Coefficient corresponding to X shear (type is char, int, float, etc)
//! @param c Coefficient corresponding to Y shear (type is char, int, float, etc)
//! @param d Coefficient corresponding to Y scale (type is char, int, float, etc)
//! @param tx Coefficient corresponding to X translation (type is char, int, float, etc)
//! @param ty Coefficient corresponding to Y translation (type is char, int, float, etc)
#define GTransformFromNumbers(a, b, c, d, tx, ty) GTransform(GTransformNumberFromNumber(a), \
GTransformNumberFromNumber(b), \
GTransformNumberFromNumber(c), \
GTransformNumberFromNumber(d), \
GTransformNumberFromNumber(tx), \
GTransformNumberFromNumber(ty))
//! This macro returns the identity transformation matrix.
//! Below is the equivalent resulting matrix:
//! t = [ 1 0 0 ]
//! [ 0 1 0 ]
//! [ 0 0 1 ]
#define GTransformIdentity() \
(GTransform) { .a = GTransformNumberOne, \
.b = GTransformNumberZero, \
.c = GTransformNumberZero, \
.d = GTransformNumberOne, \
.tx = GTransformNumberZero, \
.ty = GTransformNumberZero }
//! This macro returns a scaling transformation matrix for the corresponding input coefficients.
//! Below is the equivalent resulting matrix:
//! t = [ sx 0 0 ]
//! [ 0 sy 0 ]
//! [ 0 0 1 ]
//! @param sx X scaling factor (type is GTransformNumber)
//! @param sy Y scaling factor (type is GTransformNumber)
#define GTransformScale(sx, sy) \
(GTransform) { .a = sx, \
.b = GTransformNumberZero, \
.c = GTransformNumberZero, \
.d = sy, \
.tx = GTransformNumberZero, \
.ty = GTransformNumberZero }
//! @param sx X scaling factor (type is char, int, float, etc)
//! @param sy Y scaling factor (type is char, int, float, etc)
#define GTransformScaleFromNumber(sx, sy) \
GTransformScale(GTransformNumberFromNumber(sx), GTransformNumberFromNumber(sy))
//! This macro returns a translation transformation matrix for the corresponding input coefficients.
//! Below is the equivalent resulting matrix:
//! t = [ 1 0 0 ]
//! [ 0 1 0 ]
//! [ tx ty 1 ]
//! @param tx_v X translation factor (type is GTransformNumber)
//! @param ty_v Y translation factor (type is GTransformNumber)
#define GTransformTranslation(tx_v, ty_v) \
(GTransform) { .a = GTransformNumberOne, \
.b = GTransformNumberZero, \
.c = GTransformNumberZero, \
.d = GTransformNumberOne, \
.tx = tx_v, \
.ty = ty_v }
//! @param tx_v X translation factor (type is char, int, float, etc)
//! @param ty_v Y translation factor (type is char, int, float, etc)
#define GTransformTranslationFromNumber(tx, ty) \
GTransformTranslation(GTransformNumberFromNumber(tx), GTransformNumberFromNumber(ty))
//! @internal
//! Function that returns the rotation matrix as defined below by GTransformRotation
GTransform gtransform_init_rotation(int32_t angle);
//! This macro returns the transformation matrix for the corresponding rotation angle.
//! Below is the equivalent resulting matrix:
//! t = [ cos(angle) -sin(angle) 0 ]
//! [ sin(angle) cos(angle) 0 ]
//! [ 0 0 1 ]
//
//! The input angle corresponds to the rotation angle applied during transformation.
//! If this angle is set to 0, then the identity matrix is returned.
//! @param angle Rotation angle to apply (type is in same format as trig angle 0..TRIG_MAX_ANGLE)
#define GTransformRotation(angle) gtransform_init_rotation(angle)
//////////////////////////////////////
/// Evaluating Transforms
//////////////////////////////////////
//! Returns whether the input matrix is an identity matrix or not
//! @param t Pointer to transformation matrix to test
//! @return True if input matrix is identity; False if NULL or not identity.
bool gtransform_is_identity(const GTransform * const t);
//! Returns whether the input matrix is strictly a scaling matrix
//! @param t Pointer to transformation matrix to test
//! @return True if input matrix is only scaling X or Y; False if NULL or other coefficients set.
bool gtransform_is_only_scale(const GTransform * const t);
//! Returns whether the input matrix is strictly a translation matrix
//! @param t Pointer to transformation matrix to test
//! @return True if input matrix is only translating X or Y; False if NULL or other
//! coefficients set.
bool gtransform_is_only_translation(const GTransform * const t);
//! Returns whether the input matrix has coefficients b and c set to 0.
//! This does not check whether any other coefficients are set or not.
//! @param t Pointer to transformation matrix to test
//! @return True if input matrix is only scaling or translating X or Y; False if NULL or other.
//! coefficients set.
bool gtransform_is_only_scale_or_translation(const GTransform * const t);
//! Returns true if the two matrices are equal; false otherwise
//! Returns false if either parameter is NULL
//! @param t1 Pointer to first transformation matrix
//! @param t2 Pointer to second transformation matrix
//! @return True if both matrices are equal; False if any are NULL or if not equal.
bool gtransform_is_equal(const GTransform * const t1, const GTransform * const t2);
//////////////////////////////////////
/// Modifying Transforms
//////////////////////////////////////
//! Concatenates two transformation matrices and returns the resulting matrix in t1
//! The operation performed is t_new = t1*t2. This order is not commutative so be careful
//! when contactenating the matrices.
//! Note t_new can safely be be the same pointer as t1 or t2.
//! @param t_new Pointer to destination transformation matrix
//! @param t1 Pointer to transformation matrix to concatenate with t2 where t_new = t1*t2
//! @param t2 Pointer to transformation matrix to concatenate with t1 where t_new = t1*t2
void gtransform_concat(GTransform *t_new, const GTransform *t1, const GTransform * t2);
//! Updates the input transformation matrix by applying a translation.
//! This results in applying the following matrix below (i.e. t_new = t_scale*t):
//! t_scale = [ sx 0 0 ]
//! [ 0 sy 0 ]
//! [ 0 0 1 ]
//! Note t_new can safely be be the same pointer as t.
//! @param t_new Pointer to destination transformation matrix
//! @param t Pointer to transformation matrix that will be scaled
//! @param sx X scaling factor
//! @param sy Y scaling factor
void gtransform_scale(GTransform *t_new, GTransform *t, GTransformNumber sx, GTransformNumber sy);
//! Similar to gtransform_scale but with native number types (i.e. char, int, float, etc)
//! @param t_new Pointer to destination transformation matrix
//! @param t Pointer to transformation matrix that will be scaled
//! @param sx X scaling factor (type is char, int, float, etc)
//! @param sy Y scaling factor (type is char, int, float, etc)
#define gtransform_scale_number(t_new, t, sx, sy) \
gtransform_scale(t_new, t, \
GTransformNumberFromNumber(sx), GTransformNumberFromNumber(sy))
//! Updates the input transformation matrix by applying a translation.
//! This results in applying the following matrix below (i.e. t_new = t_translation*t):
//! t_translation = [ 1 0 0 ]
//! [ 0 1 0 ]
//! [ tx ty 1 ]
//! Note t_new can safely be be the same pointer as t.
//! @param t_new Pointer to destination transformation matrix
//! @param t Pointer to transformation matrix that will be translated
//! @param tx X translation factor
//! @param ty Y translation factor
void gtransform_translate(GTransform *t_new, GTransform *t,
GTransformNumber tx, GTransformNumber ty);
//! Similar to gtransform_translate but with native number types (i.e. char, int, float, etc)
//! @param t_new Pointer to destination transformation matrix
//! @param t Pointer to transformation matrix that will be translated
//! @param tx X translation factor (type is char, int, float, etc)
//! @param ty Y translation factor (type is char, int, float, etc)
#define gtransform_translate_number(t_new, t, tx, ty) \
gtransform_translate(t_new, t, \
GTransformNumberFromNumber(tx), GTransformNumberFromNumber(ty))
//! Updates the input transformation matrix by applying a rotation of angle degrees.
//! This results in applying the following matrix below (i.e. t_new = tr*t):
//! tr = [ cos(angle) -sin(angle) 0 ]
//! [ sin(angle) cos(angle) 0 ]
//! [ 0 0 1 ]
//! Note t_new can safely be be the same pointer as t.
//! @param t_new Pointer to destination transformation matrix
//! @param t Pointer to transformation matrix that will be rotated
//! @param angle Rotation angle to apply (type is in same format as trig angle 0..TRIG_MAX_ANGLE)
void gtransform_rotate(GTransform *t_new, GTransform *t, int32_t angle);
//! Returns the inversion of a given transformation matrix t in t_new.
//! Function returns true if operation is successful; false if the matrix cannot be inverted
//! If the matrix cannot be inverted, then the contents of t will be copied to t_new.
//! Note t_new can safely be be the same pointer as t.
//! @param t_new Pointer to destination transformation matrix
//! @param t Pointer to transformation matrix that will be inverted
//! @return True if inversion of input t matrix exists; False otherwise or if t is NULL.
bool gtransform_invert(GTransform *t_new, GTransform *t);
//////////////////////////////////////
/// Applying Transformations
//////////////////////////////////////
//! Transforms a single GPoint (x,y) based on the transformation matrix
//! @param point GPoint to be transformed
//! @param t Pointer to transformation matrix to apply to the GPoint
//! @return GPointPrecise after transforming the GPoint; if t is NULL then just convert the
//! GPoint to a GPointPrecise.
GPointPrecise gpoint_transform(GPoint point, const GTransform * const t);
//! Transforms a single GVector (dx,dy) based on the transformation matrix
//! @param point GVector to be transformed
//! @param t Pointer to transformation matrix to apply to the GVector
//! @return GVectorPrecise after transforming the GVector; if t is NULL then just convert the
//! GVector to a GVectorPrecise.
GVectorPrecise gvector_transform(GVector vector, const GTransform * const t);
//! @} // end addtogroup GraphicsTransforms
//! @} // end addtogroup Graphics

View file

@ -0,0 +1,776 @@
/*
* 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 "gtypes.h"
#include "gcontext.h"
#include "process_management/process_manager.h"
#include "process_state/app_state/app_state.h"
#include "system/passert.h"
#include "util/math.h"
#include <stddef.h>
bool gpoint_equal(const GPoint * const point_a, const GPoint * const point_b) {
return (point_a->x == point_b->x && point_a->y == point_b->y);
}
static void prv_swap_gpoint(GPoint *a, GPoint *b) {
GPoint t = *a;
*a = *b;
*b = t;
}
void gpoint_sort(GPoint *points, size_t num_points, GPointComparator comparator, void *context,
bool reverse) {
for (size_t i = 0; i < num_points; i++) {
for (size_t j = i + 1; j < num_points; j++) {
int cmp = comparator(&points[i], &points[j], context);
if (reverse ? cmp < 0 : cmp > 0) {
prv_swap_gpoint(&points[i], &points[j]);
}
}
}
}
bool gpointprecise_equal(const GPointPrecise * const pointP_a,
const GPointPrecise * const pointP_b) {
return ((pointP_a->x.raw_value == pointP_b->x.raw_value) &&
(pointP_a->y.raw_value == pointP_b->y.raw_value));
}
GPointPrecise gpointprecise_midpoint(const GPointPrecise a,
const GPointPrecise b) {
return GPointPrecise(
(int16_t)(((int32_t)a.x.raw_value + (int32_t)b.x.raw_value) / 2),
(int16_t)(((int32_t)a.y.raw_value + (int32_t)b.y.raw_value) / 2));
}
GPointPrecise gpointprecise_add(const GPointPrecise a,
const GPointPrecise b) {
return GPointPrecise(a.x.raw_value + b.x.raw_value,
a.y.raw_value + b.y.raw_value);
}
GPointPrecise gpointprecise_sub(const GPointPrecise a,
const GPointPrecise b) {
return GPointPrecise(a.x.raw_value - b.x.raw_value,
a.y.raw_value - b.y.raw_value);
}
bool gvector_equal(const GVector * const vector_a, const GVector * const vector_b) {
return (vector_a->dx == vector_b->dx && vector_a->dy == vector_b->dy);
}
bool gvectorprecise_equal(const GVectorPrecise * const vectorP_a,
const GVectorPrecise * const vectorP_b) {
return ((vectorP_a->dx.raw_value == vectorP_b->dx.raw_value) &&
(vectorP_a->dy.raw_value == vectorP_b->dy.raw_value));
}
bool gsize_equal(const GSize *size_a, const GSize *size_b) {
return (size_a->w == size_b->w && size_a->h == size_b->h);
}
bool grect_equal(const GRect* const r0, const GRect* const r1) {
return ((r0->origin.x == r1->origin.x) &&
(r0->origin.y == r1->origin.y) &&
(r0->size.w == r1->size.w) &&
(r0->size.h == r1->size.h));
}
bool grect_is_empty(const GRect* const rect) {
return (rect->size.h == 0 || rect->size.w == 0);
}
void grect_standardize(GRect *rect) {
if (rect->size.w < 0) {
rect->origin.x += rect->size.w;
rect->size.w = -rect->size.w;
}
if (rect->size.h < 0) {
rect->origin.y += rect->size.h;
rect->size.h = -rect->size.h;
}
}
void grect_clip(GRect *rect_to_clip, const GRect * const rect_clipper) {
int16_t overflow;
if (rect_to_clip->origin.x < rect_clipper->origin.x) {
overflow = rect_clipper->origin.x - rect_to_clip->origin.x;
if (overflow > rect_to_clip->size.w) {
rect_to_clip->size.w = 0;
} else {
rect_to_clip->size.w -= overflow;
}
rect_to_clip->origin.x = rect_clipper->origin.x;
} else if (rect_to_clip->origin.x > rect_clipper->origin.x + rect_clipper->size.w) {
rect_to_clip->origin.x = rect_clipper->origin.x + rect_clipper->size.w;
rect_to_clip->size.w = 0;
}
overflow = rect_to_clip->origin.x + rect_to_clip->size.w - (rect_clipper->origin.x + rect_clipper->size.w);
if (overflow > 0) {
rect_to_clip->size.w -= overflow;
}
if (rect_to_clip->origin.y < rect_clipper->origin.y) {
overflow = rect_clipper->origin.y - rect_to_clip->origin.y;
if (overflow > rect_to_clip->size.h) {
rect_to_clip->size.h = 0;
} else {
rect_to_clip->size.h -= overflow;
}
rect_to_clip->origin.y = rect_clipper->origin.y;
} else if (rect_to_clip->origin.y > rect_clipper->origin.y + rect_clipper->size.h) {
rect_to_clip->origin.y = rect_clipper->origin.y + rect_clipper->size.h;
rect_to_clip->size.h = 0;
}
overflow = rect_to_clip->origin.y + rect_to_clip->size.h - (rect_clipper->origin.y + rect_clipper->size.h);
if (overflow > 0) {
rect_to_clip->size.h -= overflow;
}
}
GRect grect_union(const GRect *r1, const GRect *r2) {
GRect s_r1 = *r1;
GRect s_r2 = *r2;
grect_standardize(&s_r1);
grect_standardize(&s_r2);
const uint8_t min_x = MIN(s_r2.origin.x, s_r1.origin.x);
const uint8_t min_y = MIN(s_r2.origin.y, s_r1.origin.y);
const uint8_t max_x = MAX(s_r2.origin.x + s_r2.size.w,
s_r1.origin.x + s_r1.size.w);
const uint8_t max_y = MAX(s_r2.origin.y + s_r2.size.h,
s_r1.origin.y + s_r1.size.h);
GRect result = GRect(min_x, min_y, max_x - min_x, max_y - min_y);
return result;
}
GPoint grect_center_point(const GRect *rect) {
return GPoint(rect->origin.x + (rect->size.w / 2), rect->origin.y + (rect->size.h / 2));
}
bool grect_contains_point(const GRect *rect, const GPoint *point) {
int16_t min_x = rect->origin.x;
int16_t max_x = rect->origin.x + rect->size.w;
if (min_x > max_x) {
// edge case for non-standardized rects:
int16_t temp = max_x;
max_x = min_x;
min_x = temp;
}
int16_t min_y = rect->origin.y;
int16_t max_y = rect->origin.y + rect->size.h;
if (min_y > max_y) {
// edge case for non-standardized rects:
int16_t temp = max_y;
max_y = min_y;
min_y = temp;
}
return (point->x >= min_x && point->x < max_x &&
point->y >= min_y && point->y < max_y);
}
void grect_align(GRect *rect, const GRect *inside_rect, const GAlign alignment, const bool clip) {
if (clip) {
if (rect->size.w > inside_rect->size.w) {
rect->size.w = inside_rect->size.w;
}
if (rect->size.h > inside_rect->size.h) {
rect->size.h = inside_rect->size.h;
}
}
switch (alignment) {
case GAlignCenter: {
rect->origin.x = ((inside_rect->size.w - rect->size.w) / 2) + inside_rect->origin.x;
rect->origin.y = ((inside_rect->size.h - rect->size.h) / 2) + inside_rect->origin.y;
return;
}
case GAlignTopLeft: {
rect->origin.x = inside_rect->origin.x;
rect->origin.y = + inside_rect->origin.y;
return;
}
case GAlignTopRight: {
rect->origin.x = (inside_rect->size.w - rect->size.w) + inside_rect->origin.x;
rect->origin.y = + inside_rect->origin.y;
return;
}
case GAlignTop: {
rect->origin.x = ((inside_rect->size.w - rect->size.w) / 2) + inside_rect->origin.x;
rect->origin.y = + inside_rect->origin.y;
return;
}
case GAlignLeft: {
rect->origin.x = inside_rect->origin.x;
rect->origin.y = ((inside_rect->size.h - rect->size.h) / 2) + inside_rect->origin.y;
return;
}
case GAlignBottom: {
rect->origin.x = ((inside_rect->size.w - rect->size.w) / 2) + inside_rect->origin.x;
rect->origin.y = (inside_rect->size.h - rect->size.h) + inside_rect->origin.y;
return;
}
case GAlignRight: {
rect->origin.x = (inside_rect->size.w - rect->size.w) + inside_rect->origin.x;
rect->origin.y = ((inside_rect->size.h - rect->size.h) / 2) + inside_rect->origin.y;
return;
}
case GAlignBottomRight: {
rect->origin.x = (inside_rect->size.w - rect->size.w) + inside_rect->origin.x;
rect->origin.y = (inside_rect->size.h - rect->size.h) + inside_rect->origin.y;
return;
}
case GAlignBottomLeft: {
rect->origin.x = inside_rect->origin.x;
rect->origin.y = (inside_rect->size.h - rect->size.h) + inside_rect->origin.y;
return;
}
}
}
GRect grect_crop(GRect rect, const int32_t crop_size_px) {
int16_t cropped_width = rect.size.w - 2 * crop_size_px;
int16_t cropped_height = rect.size.h - 2 * crop_size_px;
PBL_ASSERTN(cropped_width >= 0);
PBL_ASSERTN(cropped_height >= 0);
return grect_inset_internal(rect, crop_size_px, crop_size_px);
}
GRect grect_inset_internal(GRect rect, int16_t dx, int16_t dy) {
return grect_inset(rect,
(GEdgeInsets) {.top = dy, .right = dx, .bottom = dy, .left = dx});
}
GRect grect_inset(GRect r, GEdgeInsets insets) {
grect_standardize(&r);
const int16_t new_width = r.size.w - insets.left - insets.right;
const int16_t new_height = r.size.h - insets.top - insets.bottom;
if (new_width < 0 || new_height < 0) {
return GRectZero;
}
return GRect(r.origin.x + insets.left, r.origin.y + insets.top, new_width, new_height);
}
GPoint gpoint_to_global_coordinates(const GPoint point, GContext *ctx) {
return gpoint_add(point, ctx->draw_state.drawing_box.origin);
}
GPoint gpoint_to_local_coordinates(const GPoint point, GContext *ctx) {
return gpoint_sub(point, ctx->draw_state.drawing_box.origin);
}
GRect grect_to_global_coordinates(const GRect rect, GContext *ctx) {
GRect translated_rect = {
.origin.x = ctx->draw_state.drawing_box.origin.x + rect.origin.x,
.origin.y = ctx->draw_state.drawing_box.origin.y + rect.origin.y,
.size = rect.size,
};
return translated_rect;
}
GRect grect_to_local_coordinates(const GRect rect, GContext *ctx) {
GRect translated_rect = {
.origin.x = -ctx->draw_state.drawing_box.origin.x + rect.origin.x,
.origin.y = -ctx->draw_state.drawing_box.origin.y + rect.origin.y,
.size = rect.size,
};
return translated_rect;
}
bool grect_overlaps_grect(const GRect *r1, const GRect *r2) {
if ((r1->origin.x < (r2->origin.x + r2->size.w)) && // Left edge of r1 not past r2's right edge
((r1->origin.x + r1->size.w) > r2->origin.x) && // Right edge of r1 is right of r2's left edge
(r1->origin.y < (r2->origin.y + r2->size.h)) && // Top edge r1 not below r2's bottom edge
((r1->origin.y + r1->size.h) > r2->origin.y)) { // Bottom edge r1 not above r2's top edge
return true;
}
return false;
}
void grect_precise_standardize(GRectPrecise *rect) {
if (rect->size.w.raw_value < 0) {
rect->origin.x.raw_value += rect->size.w.raw_value;
rect->size.w.raw_value = -rect->size.w.raw_value;
}
if (rect->size.h.raw_value < 0) {
rect->origin.y.raw_value += rect->size.h.raw_value;
rect->size.h.raw_value = -rect->size.h.raw_value;
}
}
bool gcolor_is_transparent(GColor8 color) {
// Mimic a "closest color" behaviour, since we do not have blending.
return (color.a <= 1);
}
GColor8 gcolor_closest_opaque(GColor8 color) {
if (gcolor_is_transparent(color)) {
return GColorClear;
}
color.a = 3;
return color;
}
static int32_t prv_get_luminance_10000(GColor8 color) {
// fixed-point implementation (to base 10,000 decimal) of
// luminance = (0.2126*R + 0.7152*G + 0.0722*B)
// as in http://stackoverflow.com/a/596243
return (2126 * color.r + 7152 * color.g + 722 * color.b) / 3;
}
GColor8 gcolor_get_bw(GColor8 color) {
if (gcolor_is_transparent(color)) {
return GColorClear;
}
const int32_t MAX_LUMINANCE = 10000;
const int32_t luminance = prv_get_luminance_10000(color);
if (luminance < MAX_LUMINANCE / 2) {
return GColorBlack;
}
else {
return GColorWhite;
}
}
GColor8 gcolor_get_grayscale(GColor8 color) {
if (gcolor_is_transparent(color)) {
return GColorClear;
}
const int32_t DARK_GRAY_LUMINANCE = 3333;
const int32_t LIGHT_GRAY_LUMINANCE = 6666;
const int32_t luminance = prv_get_luminance_10000(color);
if (luminance < DARK_GRAY_LUMINANCE) {
return GColorBlack;
}
else if (luminance < (LIGHT_GRAY_LUMINANCE + DARK_GRAY_LUMINANCE) / 2) {
return GColorDarkGray;
}
else if (luminance <= LIGHT_GRAY_LUMINANCE) {
return GColorLightGray;
}
else {
return GColorWhite;
}
}
GColor8 gcolor_legible_over(GColor8 background_color) {
background_color = gcolor_closest_opaque(background_color);
// special cases - needed to fulfill test_graphics_colors__inverted_readable_color()
switch (background_color.argb) {
case GColorClearARGB8:
return GColorClear;
}
const int32_t luminance = prv_get_luminance_10000(background_color);
// this value is derived from test_graphics_colors__inverted_readable_color()
const int32_t MAGIC_THRESHOLD = 4510;
const bool bright = luminance >= MAGIC_THRESHOLD;
return bright ? GColorBlack : GColorWhite;
}
BitmapInfo gbitmap_get_info(const GBitmap *bitmap) {
if (!bitmap) {
return (BitmapInfo) { 0 };
}
// In 2.x, GBitmap was exposed and info_flags was only used for keeping track of heap allocation.
// Some apps were constructing their own GBitmaps, and not setting info_flags.
// For a legacy2 app, zero out all info fields except for bitmap heap allocation, and assume
// that if the flag is set and the bitmap is allocated on the heap, the flag is valid.
if (process_manager_compiled_with_legacy2_sdk()) {
Heap * heap = app_state_get_heap();
return (BitmapInfo) {
.is_bitmap_heap_allocated =
bitmap->info.is_bitmap_heap_allocated && heap_is_allocated(heap, bitmap->addr),
};
}
return bitmap->info;
}
bool gcolor_is_invisible(GColor8 color) {
return (color.a == 0);
}
#define RGB_LOOKUP_TABLE_SIZE (64)
const GColor8Component g_color_luminance_lookup[RGB_LOOKUP_TABLE_SIZE] = {
0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3,
};
// Table with blended colors rendered by Photoshop
// 64 rows for 64 source colors and 64 columns for 64 destination colors
// Values below were calculated for 33% blending
// 66% blending can be achieved by transforming the table around diagonal axis
static const uint8_t s_blending_lookup_33_percent[RGB_LOOKUP_TABLE_SIZE * RGB_LOOKUP_TABLE_SIZE] = {
0xc0, 0xc1, 0xc1, 0xc2, 0xc4, 0xc5, 0xc5, 0xc6, 0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea,
0xc0, 0xc1, 0xc2, 0xc2, 0xc4, 0xc5, 0xc6, 0xc6, 0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea,
0xc1, 0xc1, 0xc2, 0xc3, 0xc5, 0xc5, 0xc6, 0xc7, 0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb,
0xc1, 0xc2, 0xc2, 0xc3, 0xc5, 0xc6, 0xc6, 0xc7, 0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb,
0xc0, 0xc1, 0xc1, 0xc2, 0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca, 0xc8, 0xc9, 0xc9, 0xca,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea,
0xc0, 0xc1, 0xc2, 0xc2, 0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca, 0xc8, 0xc9, 0xca, 0xca,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea,
0xc1, 0xc1, 0xc2, 0xc3, 0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb, 0xc9, 0xc9, 0xca, 0xcb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb,
0xc1, 0xc2, 0xc2, 0xc3, 0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb, 0xc9, 0xca, 0xca, 0xcb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb,
0xc4, 0xc5, 0xc5, 0xc6, 0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca, 0xcc, 0xcd, 0xcd, 0xce,
0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xc4, 0xc5, 0xc6, 0xc6, 0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca, 0xcc, 0xcd, 0xce, 0xce,
0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xc5, 0xc5, 0xc6, 0xc7, 0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb, 0xcd, 0xcd, 0xce, 0xcf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xc5, 0xc6, 0xc6, 0xc7, 0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb, 0xcd, 0xce, 0xce, 0xcf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca, 0xc8, 0xc9, 0xc9, 0xca, 0xcc, 0xcd, 0xcd, 0xce,
0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca, 0xc8, 0xc9, 0xca, 0xca, 0xcc, 0xcd, 0xce, 0xce,
0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb, 0xc9, 0xc9, 0xca, 0xcb, 0xcd, 0xcd, 0xce, 0xcf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb, 0xc9, 0xca, 0xca, 0xcb, 0xcd, 0xce, 0xce, 0xcf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xc0, 0xc1, 0xc1, 0xc2, 0xc4, 0xc5, 0xc5, 0xc6, 0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea,
0xc0, 0xc1, 0xc2, 0xc2, 0xc4, 0xc5, 0xc6, 0xc6, 0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea,
0xc1, 0xc1, 0xc2, 0xc3, 0xc5, 0xc5, 0xc6, 0xc7, 0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb,
0xc1, 0xc2, 0xc2, 0xc3, 0xc5, 0xc6, 0xc6, 0xc7, 0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb,
0xc0, 0xc1, 0xc1, 0xc2, 0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca, 0xc8, 0xc9, 0xc9, 0xca,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea,
0xc0, 0xc1, 0xc2, 0xc2, 0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca, 0xc8, 0xc9, 0xca, 0xca,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea,
0xc1, 0xc1, 0xc2, 0xc3, 0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb, 0xc9, 0xc9, 0xca, 0xcb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb,
0xc1, 0xc2, 0xc2, 0xc3, 0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb, 0xc9, 0xca, 0xca, 0xcb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb,
0xc4, 0xc5, 0xc5, 0xc6, 0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca, 0xcc, 0xcd, 0xcd, 0xce,
0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xc4, 0xc5, 0xc6, 0xc6, 0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca, 0xcc, 0xcd, 0xce, 0xce,
0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xc5, 0xc5, 0xc6, 0xc7, 0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb, 0xcd, 0xcd, 0xce, 0xcf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xc5, 0xc6, 0xc6, 0xc7, 0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb, 0xcd, 0xce, 0xce, 0xcf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xc4, 0xc5, 0xc5, 0xc6, 0xc8, 0xc9, 0xc9, 0xca, 0xc8, 0xc9, 0xc9, 0xca, 0xcc, 0xcd, 0xcd, 0xce,
0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xc4, 0xc5, 0xc6, 0xc6, 0xc8, 0xc9, 0xca, 0xca, 0xc8, 0xc9, 0xca, 0xca, 0xcc, 0xcd, 0xce, 0xce,
0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xc5, 0xc5, 0xc6, 0xc7, 0xc9, 0xc9, 0xca, 0xcb, 0xc9, 0xc9, 0xca, 0xcb, 0xcd, 0xcd, 0xce, 0xcf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xc5, 0xc6, 0xc6, 0xc7, 0xc9, 0xca, 0xca, 0xcb, 0xc9, 0xca, 0xca, 0xcb, 0xcd, 0xce, 0xce, 0xcf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea,
0xf0, 0xf1, 0xf1, 0xf2, 0xf4, 0xf5, 0xf5, 0xf6, 0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea,
0xf0, 0xf1, 0xf2, 0xf2, 0xf4, 0xf5, 0xf6, 0xf6, 0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb,
0xf1, 0xf1, 0xf2, 0xf3, 0xf5, 0xf5, 0xf6, 0xf7, 0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb,
0xf1, 0xf2, 0xf2, 0xf3, 0xf5, 0xf6, 0xf6, 0xf7, 0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea,
0xf0, 0xf1, 0xf1, 0xf2, 0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa, 0xf8, 0xf9, 0xf9, 0xfa,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea,
0xf0, 0xf1, 0xf2, 0xf2, 0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa, 0xf8, 0xf9, 0xfa, 0xfa,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb,
0xf1, 0xf1, 0xf2, 0xf3, 0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb, 0xf9, 0xf9, 0xfa, 0xfb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb,
0xf1, 0xf2, 0xf2, 0xf3, 0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb, 0xf9, 0xfa, 0xfa, 0xfb,
0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xf4, 0xf5, 0xf5, 0xf6, 0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa, 0xfc, 0xfd, 0xfd, 0xfe,
0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xf4, 0xf5, 0xf6, 0xf6, 0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa, 0xfc, 0xfd, 0xfe, 0xfe,
0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xf5, 0xf5, 0xf6, 0xf7, 0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb, 0xfd, 0xfd, 0xfe, 0xff,
0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xf5, 0xf6, 0xf6, 0xf7, 0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb, 0xfd, 0xfe, 0xfe, 0xff,
0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa, 0xf8, 0xf9, 0xf9, 0xfa, 0xfc, 0xfd, 0xfd, 0xfe,
0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa, 0xf8, 0xf9, 0xfa, 0xfa, 0xfc, 0xfd, 0xfe, 0xfe,
0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb, 0xf9, 0xf9, 0xfa, 0xfb, 0xfd, 0xfd, 0xfe, 0xff,
0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb, 0xf9, 0xfa, 0xfa, 0xfb, 0xfd, 0xfe, 0xfe, 0xff,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea,
0xf0, 0xf1, 0xf1, 0xf2, 0xf4, 0xf5, 0xf5, 0xf6, 0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea,
0xf0, 0xf1, 0xf2, 0xf2, 0xf4, 0xf5, 0xf6, 0xf6, 0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb,
0xf1, 0xf1, 0xf2, 0xf3, 0xf5, 0xf5, 0xf6, 0xf7, 0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb,
0xf1, 0xf2, 0xf2, 0xf3, 0xf5, 0xf6, 0xf6, 0xf7, 0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb,
0xd0, 0xd1, 0xd1, 0xd2, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea,
0xe0, 0xe1, 0xe1, 0xe2, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea,
0xf0, 0xf1, 0xf1, 0xf2, 0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa, 0xf8, 0xf9, 0xf9, 0xfa,
0xd0, 0xd1, 0xd2, 0xd2, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea,
0xe0, 0xe1, 0xe2, 0xe2, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea,
0xf0, 0xf1, 0xf2, 0xf2, 0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa, 0xf8, 0xf9, 0xfa, 0xfa,
0xd1, 0xd1, 0xd2, 0xd3, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb,
0xe1, 0xe1, 0xe2, 0xe3, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb,
0xf1, 0xf1, 0xf2, 0xf3, 0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb, 0xf9, 0xf9, 0xfa, 0xfb,
0xd1, 0xd2, 0xd2, 0xd3, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb,
0xe1, 0xe2, 0xe2, 0xe3, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb,
0xf1, 0xf2, 0xf2, 0xf3, 0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb, 0xf9, 0xfa, 0xfa, 0xfb,
0xd4, 0xd5, 0xd5, 0xd6, 0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xe4, 0xe5, 0xe5, 0xe6, 0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xf4, 0xf5, 0xf5, 0xf6, 0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa, 0xfc, 0xfd, 0xfd, 0xfe,
0xd4, 0xd5, 0xd6, 0xd6, 0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xe4, 0xe5, 0xe6, 0xe6, 0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xf4, 0xf5, 0xf6, 0xf6, 0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa, 0xfc, 0xfd, 0xfe, 0xfe,
0xd5, 0xd5, 0xd6, 0xd7, 0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xe5, 0xe5, 0xe6, 0xe7, 0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xf5, 0xf5, 0xf6, 0xf7, 0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb, 0xfd, 0xfd, 0xfe, 0xff,
0xd5, 0xd6, 0xd6, 0xd7, 0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xe5, 0xe6, 0xe6, 0xe7, 0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xf5, 0xf6, 0xf6, 0xf7, 0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb, 0xfd, 0xfe, 0xfe, 0xff,
0xd4, 0xd5, 0xd5, 0xd6, 0xd8, 0xd9, 0xd9, 0xda, 0xd8, 0xd9, 0xd9, 0xda, 0xdc, 0xdd, 0xdd, 0xde,
0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xe4, 0xe5, 0xe5, 0xe6, 0xe8, 0xe9, 0xe9, 0xea, 0xe8, 0xe9, 0xe9, 0xea, 0xec, 0xed, 0xed, 0xee,
0xf4, 0xf5, 0xf5, 0xf6, 0xf8, 0xf9, 0xf9, 0xfa, 0xf8, 0xf9, 0xf9, 0xfa, 0xfc, 0xfd, 0xfd, 0xfe,
0xd4, 0xd5, 0xd6, 0xd6, 0xd8, 0xd9, 0xda, 0xda, 0xd8, 0xd9, 0xda, 0xda, 0xdc, 0xdd, 0xde, 0xde,
0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xe4, 0xe5, 0xe6, 0xe6, 0xe8, 0xe9, 0xea, 0xea, 0xe8, 0xe9, 0xea, 0xea, 0xec, 0xed, 0xee, 0xee,
0xf4, 0xf5, 0xf6, 0xf6, 0xf8, 0xf9, 0xfa, 0xfa, 0xf8, 0xf9, 0xfa, 0xfa, 0xfc, 0xfd, 0xfe, 0xfe,
0xd5, 0xd5, 0xd6, 0xd7, 0xd9, 0xd9, 0xda, 0xdb, 0xd9, 0xd9, 0xda, 0xdb, 0xdd, 0xdd, 0xde, 0xdf,
0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xe5, 0xe5, 0xe6, 0xe7, 0xe9, 0xe9, 0xea, 0xeb, 0xe9, 0xe9, 0xea, 0xeb, 0xed, 0xed, 0xee, 0xef,
0xf5, 0xf5, 0xf6, 0xf7, 0xf9, 0xf9, 0xfa, 0xfb, 0xf9, 0xf9, 0xfa, 0xfb, 0xfd, 0xfd, 0xfe, 0xff,
0xd5, 0xd6, 0xd6, 0xd7, 0xd9, 0xda, 0xda, 0xdb, 0xd9, 0xda, 0xda, 0xdb, 0xdd, 0xde, 0xde, 0xdf,
0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xe5, 0xe6, 0xe6, 0xe7, 0xe9, 0xea, 0xea, 0xeb, 0xe9, 0xea, 0xea, 0xeb, 0xed, 0xee, 0xee, 0xef,
0xf5, 0xf6, 0xf6, 0xf7, 0xf9, 0xfa, 0xfa, 0xfb, 0xf9, 0xfa, 0xfa, 0xfb, 0xfd, 0xfe, 0xfe, 0xff,
};
GColor8 gcolor_blend(GColor8 src_color, GColor8 dest_color, uint8_t blending_factor) {
// Mask for masking out alpha channel and retrieving number of the color
const uint8_t MASK_RGB = 0b00111111;
switch (blending_factor) {
case 0:
// Fast path: 0%, no-op!
return dest_color;
case 1:
// Lookup: 33%
return (GColor8) {
.argb = s_blending_lookup_33_percent[(dest_color.argb & MASK_RGB) +
RGB_LOOKUP_TABLE_SIZE * (src_color.argb & MASK_RGB)],
};
case 2:
// Lookup: 66% - same as mirrored 33% results
return (GColor8) {
.argb = s_blending_lookup_33_percent[(src_color.argb & MASK_RGB) +
RGB_LOOKUP_TABLE_SIZE * (dest_color.argb & MASK_RGB)],
};
case 3:
// Fast path: 100%
return src_color;
default:
// Something went utterly wrong - proceed to throw up
WTF;
}
}
GColor8 gcolor_alpha_blend(GColor8 src_color, GColor8 dest_color) {
return gcolor_blend(src_color, dest_color, src_color.a);
}
void gcolor_tint_luminance_lookup_table_init(
GColor8 tint_color, GColor8 *lookup_table_out) {
PBL_ASSERTN(lookup_table_out);
// Inverting the tint color this way inverts the alpha channel too, but we set the alpha of all
// colors in the lookup table to the original tint color's alpha in the loop below
const GColor8 inverted_tint_color = (GColor8) { .argb = ~tint_color.argb };
for (GColor8Component luminance_index = 0; luminance_index < GCOLOR8_COMPONENT_NUM_VALUES;
luminance_index++) {
GColor8 blended_color = gcolor_blend(inverted_tint_color, tint_color, luminance_index);
// Preserve the alpha of the tint color after the blend
blended_color.a = tint_color.a;
lookup_table_out[luminance_index] = blended_color;
}
}
GColor8 gcolor_perform_lookup_using_color_luminance_and_multiply_alpha(
GColor8 src_color, const GColor8 lookup_table[GCOLOR8_COMPONENT_NUM_VALUES]) {
PBL_ASSERTN(lookup_table);
const GColor8Component src_color_luminance = gcolor_get_luminance(src_color);
GColor8 result = lookup_table[src_color_luminance];
result.a = gcolor_component_multiply(src_color.a, result.a);
return result;
}
GColor8 gcolor_tint_using_luminance_and_multiply_alpha(GColor8 src_color, GColor8 tint_color) {
GColor8 tint_luminance_lookup_table[GCOLOR8_COMPONENT_NUM_VALUES];
gcolor_tint_luminance_lookup_table_init(tint_color, tint_luminance_lookup_table);
return gcolor_perform_lookup_using_color_luminance_and_multiply_alpha(
src_color, tint_luminance_lookup_table);
}
static const GColor8Component s_color_component_multiplication_lookup[16] = {
0, 0, 0, 0,
0, 0, 1, 1,
0, 1, 1, 2,
0, 1, 2, 3,
};
GColor8Component gcolor_component_multiply(GColor8Component a, GColor8Component b) {
// TODO PBL-37522: Benchmark using arithmetic for this expression
return s_color_component_multiplication_lookup[(a << 2) | b];
}
void grange_clip(GRange *range_to_clip, const GRange * const range_clipper) {
int16_t start = range_to_clip->origin;
int16_t end = range_to_clip->origin + range_to_clip->size;
start = CLIP(start, range_clipper->origin, range_clipper->origin + range_clipper->size);
end = CLIP(end, range_clipper->origin, range_clipper->origin + range_clipper->size);
range_to_clip->origin = start;
range_to_clip->size = end - start;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
/*
* 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 "applib/graphics/perimeter.h"
#include "system/passert.h"
#include "util/math.h"
static uint16_t prv_triangle_side(uint16_t hypotenuse, uint16_t side) {
// third side of triangle based on pythagorean theorem
return integer_sqrt(ABS(((uint32_t)hypotenuse * hypotenuse) - ((uint32_t)side * side)));
}
T_STATIC GRangeHorizontal perimeter_for_circle(GRangeVertical vertical_range, GPoint center,
int32_t radius) {
radius = MAX(0, radius);
int32_t height = 0;
int32_t width = 0;
const int32_t top = center.y - radius;
const int32_t bottom = center.y + radius;
int32_t range_start = vertical_range.origin_y;
int32_t range_end = vertical_range.origin_y + vertical_range.size_h;
// Check if both top and bottom are outside but not surrounding the perimeter
if ((range_start < top && range_end < top) ||
(range_start > bottom && range_end > bottom)) {
return (GRangeHorizontal){0, 0};
}
range_start = CLIP(range_start, top, bottom);
range_end = CLIP(range_end, top, bottom);
// height of triangle from center to range start
height = ABS(center.y - range_start);
const int32_t start_width = prv_triangle_side(radius, height);
// height of triangle from center to range end
height = ABS(center.y - range_end);
const int32_t end_width = prv_triangle_side(radius, height);
width = MIN(start_width, end_width);
return (GRangeHorizontal){.origin_x = center.x - width, .size_w = width * 2};
}
T_STATIC GRangeHorizontal perimeter_for_display_round(const GPerimeter *perimeter,
const GSize *ctx_size,
GRangeVertical vertical_range,
uint16_t inset) {
const GRect frame = (GRect) { GPointZero, *ctx_size };
const GPoint center = grect_center_point(&frame);
const int32_t radius = grect_shortest_side(frame) / 2 - inset;
return perimeter_for_circle(vertical_range, center, radius);
}
T_STATIC GRangeHorizontal perimeter_for_display_rect(const GPerimeter *perimeter,
const GSize *ctx_size,
GRangeVertical vertical_range,
uint16_t inset) {
return (GRangeHorizontal){.origin_x = inset, .size_w = MAX(0, ctx_size->w - 2 * inset)};
}
const GPerimeter * const g_perimeter_for_display = &(const GPerimeter) {
.callback = PBL_IF_RECT_ELSE(perimeter_for_display_rect, perimeter_for_display_round),
};

View file

@ -0,0 +1,33 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
typedef struct GPerimeter GPerimeter;
//! @internal
typedef GRangeHorizontal (*GPerimeterCallback)(const GPerimeter *perimeter, const GSize *ctx_size,
GRangeVertical vertical_range, uint16_t inset);
//! @internal
typedef struct GPerimeter {
GPerimeterCallback callback;
} GPerimeter;
//! @internal
extern const GPerimeter * const g_perimeter_for_display;

View file

@ -0,0 +1,285 @@
/*
* 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.
*/
#pragma once
#include "applib/graphics/gtypes.h"
#include "applib/graphics/perimeter.h"
#include "applib/fonts/fonts.h"
#include <stdint.h>
#include <stdbool.h>
//! @addtogroup Graphics
//! @{
//! @addtogroup TextDrawing Drawing Text
//! \brief Functions to draw text into a graphics context
//!
//! See \ref GraphicsContext for more information about the graphics context.
//!
//! Other drawing functions and related documentation:
//! * \ref Drawing
//! * \ref PathDrawing
//! * \ref GraphicsTypes
//! @{
//! Text overflow mode controls the way text overflows when the string that is drawn does not fit
//! inside the area constraint.
//! @see graphics_draw_text
//! @see text_layer_set_overflow_mode
typedef enum {
//! On overflow, wrap words to a new line below the current one. Once vertical space is consumed,
//! the last line may be clipped.
GTextOverflowModeWordWrap,
//! On overflow, wrap words to a new line below the current one.
//! Once vertical space is consumed, truncate as needed to fit a trailing ellipsis (...).
//! Clipping may occur if the vertical space cannot accomodate the first line of text.
GTextOverflowModeTrailingEllipsis,
//! Acts like \ref GTextOverflowModeTrailingEllipsis, plus trims leading and trailing newlines,
//! while treating all other newlines as spaces.
GTextOverflowModeFill
} GTextOverflowMode;
//! Text aligment controls the way the text is aligned inside the box the text is drawn into.
//! @see graphics_draw_text
//! @see text_layer_set_text_alignment
typedef enum {
//! Aligns the text to the left of the drawing box
GTextAlignmentLeft,
//! Aligns the text centered inside the drawing box
GTextAlignmentCenter,
//! Aligns the text to the right of the drawing box
GTextAlignmentRight,
} GTextAlignment;
//! @internal
typedef enum {
GVerticalAlignmentTop,
GVerticalAlignmentCenter,
GVerticalAlignmentBottom,
} GVerticalAlignment;
typedef struct {
//! Invalidate the cache if these parameters have changed
uint32_t hash;
GRect box;
GFont font;
GTextOverflowMode overflow_mode;
GTextAlignment alignment;
//! Cached parameters
GSize max_used_size; //<! Max area occupied by text in px
} TextLayout;
//! @internal
typedef struct {
const GPerimeter *impl;
uint8_t inset;
} TextLayoutFlowDataPerimeter;
//! @internal
typedef struct {
GPoint origin_on_screen;
GRangeVertical page_on_screen;
} TextLayoutFlowDataPaging;
//! @internal
typedef struct {
TextLayoutFlowDataPerimeter perimeter;
TextLayoutFlowDataPaging paging;
} TextLayoutFlowData;
//! @internal
//! Not supported in 2.X. This new structure is required to avoid breaking existing memory
//! contract with 2.X compiled apps and maintain compatibility.
typedef struct {
//! Invalidate the cache if these parameters have changed
uint32_t hash;
GRect box;
GFont font;
GTextOverflowMode overflow_mode;
GTextAlignment alignment;
//! Cached parameters
GSize max_used_size; //<! Max area occupied by text in px
//! Vertical padding in px to add to the font line height when rendering
int16_t line_spacing_delta;
//! TODO: PBL-22653 recover TextLayoutExtended padding by reducing the below types
//! Layout restriction callback shrinking text box to fit within perimeter
TextLayoutFlowData flow_data;
} TextLayoutExtended;
//! Pointer to opaque text layout cache data structure
typedef TextLayout* GTextLayoutCacheRef;
//! Describes various characteristics for text rendering and measurement.
//! @see graphics_draw_text
//! @see graphics_text_attributes_create
//! @see graphics_text_attributes_enable_screen_text_flow
//! @see graphics_text_attributes_enable_paging
typedef TextLayout GTextAttributes;
//! @internal
//! Synonym for graphic_fonts_init()
void graphics_text_init(void);
//! Draw text into the current graphics context, using the context's current text color.
//! The text will be drawn inside a box with the specified dimensions and
//! configuration, with clipping occuring automatically.
//! @param ctx The destination graphics context in which to draw
//! @param text The zero terminated UTF-8 string to draw
//! @param font The font in which the text should be set
//! @param box The bounding box in which to draw the text. The first line of text will be drawn
//! against the top of the box.
//! @param overflow_mode The overflow behavior, in case the text is larger than what fits inside
//! the box.
//! @param alignment The horizontal alignment of the text
//! @param text_attributes Optional text attributes to describe the characteristics of the text
void graphics_draw_text(GContext *ctx, const char *text, GFont const font, const GRect box,
const GTextOverflowMode overflow_mode, const GTextAlignment alignment,
GTextAttributes *text_attributes);
//! Obtain the maximum size that a text with given font, overflow mode and alignment
//! occupies within a given rectangular constraint.
//! @param ctx the current graphics context
//! @param text The zero terminated UTF-8 string for which to calculate the size
//! @param font The font in which the text should be set while calculating the size
//! @param box The bounding box in which the text should be constrained
//! @param overflow_mode The overflow behavior, in case the text is larger than what fits
//! inside the box.
//! @param alignment The horizontal alignment of the text
//! @param layout Optional layout cache data. Supply `NULL` to ignore the layout caching mechanism.
//! @return The maximum size occupied by the text
//! @note Because of an implementation detail, it is necessary to pass in the current graphics
//! context,
//! even though this function does not draw anything.
//! @internal
//! @see \ref app_get_current_graphics_context()
GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text,
GFont const font, const GRect box,
const GTextOverflowMode overflow_mode,
const GTextAlignment alignment,
GTextLayoutCacheRef layout);
//! Obtain the maximum size that a text with given font, overflow mode and alignment occupies
//! within a given rectangular constraint.
//! @param text The zero terminated UTF-8 string for which to calculate the size
//! @param font The font in which the text should be set while calculating the size
//! @param box The bounding box in which the text should be constrained
//! @param overflow_mode The overflow behavior, in case the text is larger than what fits
//! inside the box.
//! @param alignment The horizontal alignment of the text
//! @return The maximum size occupied by the text
//! @see app_graphics_text_layout_get_content_size_with_attributes
GSize app_graphics_text_layout_get_content_size(const char *text, GFont const font, const GRect box,
const GTextOverflowMode overflow_mode,
const GTextAlignment alignment);
//! Obtain the maximum size that a text with given font, overflow mode and alignment occupies
//! within a given rectangular constraint.
//! @param text The zero terminated UTF-8 string for which to calculate the size
//! @param font The font in which the text should be set while calculating the size
//! @param box The bounding box in which the text should be constrained
//! @param overflow_mode The overflow behavior, in case the text is larger than what fits
//! inside the box.
//! @param alignment The horizontal alignment of the text
//! @param text_attributes Optional text attributes to describe the characteristics of the text
//! @return The maximum size occupied by the text
//! @see app_graphics_text_layout_get_content_size
GSize app_graphics_text_layout_get_content_size_with_attributes(
const char *text, GFont const font, const GRect box, const GTextOverflowMode overflow_mode,
const GTextAlignment alignment, GTextAttributes *text_attributes);
//! @internal
//! Does the same as \ref app_graphics_text_layout_get_text_height with the provided GContext
uint16_t graphics_text_layout_get_text_height(GContext *ctx, const char *text, GFont const font,
uint16_t bounds_width,
const GTextOverflowMode overflow_mode,
const GTextAlignment alignment);
//! @internal
//! Malloc a text layout cache
void graphics_text_layout_cache_init(GTextLayoutCacheRef *layout_cache);
//! @internal
//! Free a text layout cache
void graphics_text_layout_cache_deinit(GTextLayoutCacheRef *layout_cache);
//! Creates an instance of GTextAttributes for advanced control when rendering text.
//! @return New instance of GTextAttributes
//! @see \ref graphics_draw_text
GTextAttributes *graphics_text_attributes_create(void);
//! Destroys a previously created instance of GTextAttributes
void graphics_text_attributes_destroy(GTextAttributes *text_attributes);
//! Sets the current line spacing delta for the given layout.
//! @param layout Text layout
//! @param delta The vertical line spacing delta in pixels to set for the given layout
void graphics_text_layout_set_line_spacing_delta(GTextLayoutCacheRef layout, int16_t delta);
//! Returns the current line spacing delta for the given layout.
//! @param layout Text layout
//! @return The vertical line spacing delta for the given layout
int16_t graphics_text_layout_get_line_spacing_delta(const GTextLayoutCacheRef layout);
//! Restores text flow to the rectangular default.
//! @param text_attributes The attributes for which to disable text flow
//! @see graphics_text_attributes_enable_screen_text_flow
//! @see text_layer_restore_default_text_flow_and_paging
void graphics_text_attributes_restore_default_text_flow(GTextAttributes *text_attributes);
//! Enables text flow that follows the boundaries of the screen.
//! @param text_attributes The attributes for which text flow should be enabled
//! @param inset Additional amount of pixels to inset to the inside of the screen for text flow
//! calculation. Can be zero.
//! @see graphics_text_attributes_restore_default_text_flow
//! @see text_layer_enable_screen_text_flow_and_paging
void graphics_text_attributes_enable_screen_text_flow(GTextAttributes *text_attributes,
uint8_t inset);
//! Restores paging and locked content origin to the defaults.
//! @param text_attributes The attributes for which to restore paging and locked content origin
//! @see graphics_text_attributes_enable_paging
//! @see text_layer_restore_default_text_flow_and_paging
void graphics_text_attributes_restore_default_paging(GTextAttributes *text_attributes);
//! Enables paging and locks the text flow calculation to a fixed point on the screen.
//! @param text_attributes Attributes for which to enable paging and locked content origin
//! @param content_origin_on_screen Absolute coordinate on the screen where the text content
//! starts before an animation or scrolling takes place. Usually the frame's origin of a layer
//! in screen coordinates.
//! @param paging_on_screen Rectangle in absolute coordinates on the screen that describes where
//! text content pages. Usually the container's absolute frame in screen coordinates.
//! @see graphics_text_attributes_restore_default_paging
//! @see graphics_text_attributes_enable_screen_text_flow
//! @see text_layer_enable_screen_text_flow_and_paging
//! @see layer_convert_point_to_screen
void graphics_text_attributes_enable_paging(GTextAttributes *text_attributes,
GPoint content_origin_on_screen,
GRect paging_on_screen);
//! @internal
const TextLayoutFlowData *graphics_text_layout_get_flow_data(GTextLayoutCacheRef layout);
//! @internal
void graphics_text_perimeter_debugging_enable(bool enable);
//! @} // end addtogroup TextDrawing
//! @} // end addtogroup Graphics

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,133 @@
/*
* 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.
*/
#pragma once
//! Private layout interface (ie for unit testing)
#include "util/iterator.h"
#include "applib/fonts/codepoint.h"
#include "text.h"
#include "gtypes.h"
#include "utf8.h"
#include <stdint.h>
typedef struct {
const Utf8Bounds* utf8_bounds; //<! start and end of utf-8 codepoints
GRect box;
GFont font;
GTextOverflowMode overflow_mode;
GTextAlignment alignment;
int16_t line_spacing_delta;
} TextBoxParams;
//! Parameters required to render a line
typedef struct {
utf8_t* start;
GPoint origin; //<! Relative to text_box_params origin
int16_t height_px;
int16_t width_px;
int16_t max_width_px; //<! Maximum length of the line
Codepoint suffix_codepoint;
} Line;
//! Definition of a word:
//! "A brown dog\njumps" becomes:
//! - "A"
//! - " brown" // whitespace is trimmed if word wraps
//! - " dog" // whitespace is trimmed if word wraps
//! - "\n"
//! - "jumps"
//!
//! - Word start points to first printable codepoint in word, inclusive,
//! including whitespace
//! - Word end points to codepoint after the last printable codepoint in a word,
//! excluding whitespace (eg, end of word, exclusive); note this codepoint may
//! not be valid since it may be the end of the string
//! - The preceeding whitespace of a word is trimmed if the word wraps
//! - Reserved codepoints are skipped
//! - Newlines are treated as stand-alone words so as to not mess up the height
//! and width word metrics
typedef struct {
utf8_t* start;
utf8_t* end;
int16_t width_px;
} Word;
#define WORD_EMPTY ((Word){ 0, 0, 0 })
typedef struct {
const TextBoxParams* text_box_params;
Iterator utf8_iter;
Utf8IterState utf8_iter_state;
} CharIterState;
//! Uses character iterator to iterate over characters
typedef struct {
GContext* ctx;
const TextBoxParams* text_box_params;
Word current;
} WordIterState;
#define WORD_ITER_STATE_EMPTY ((WordIterState){ 0, 0, WORD_EMPTY })
typedef struct {
GContext *ctx;
Line *current;
Iterator word_iter;
WordIterState word_iter_state;
} LineIterState;
typedef struct {
TextBoxParams text_box;
Line line;
LineIterState line_iter_state;
} TextDrawState;
void char_iter_init(Iterator* char_iter, CharIterState* char_iter_state, const TextBoxParams* const text_box_params, utf8_t* start);
void word_iter_init(Iterator* word_iter, WordIterState* word_iter_state, GContext* ctx, const TextBoxParams* const text_box_params, utf8_t* start);
void line_iter_init(Iterator* line_iter, LineIterState* line_iter_state, GContext* ctx);
bool word_init(GContext* ctx, Word* word, const TextBoxParams* const text_box_params, utf8_t* start);
bool char_iter_next(IteratorState state);
bool char_iter_prev(IteratorState state);
bool word_iter_next(IteratorState state);
bool line_iter_next(IteratorState state);
typedef void (*LastLineCallback)(GContext* ctx, Line* line,
const TextBoxParams* const text_box_params,
const bool is_text_remaining);
typedef void (*RenderLineCallback)(GContext* ctx, Line* line,
const TextBoxParams* const text_box_params);
typedef void (*LayoutUpdateCallback)(TextLayout* layout, Line* line,
const TextBoxParams* const text_box_params);
typedef bool (*StopConditionCallback)(GContext* ctx, Line* line,
const TextBoxParams* const text_box_params);
bool line_add_word(GContext* ctx, Line* line, Word* word, const TextBoxParams* const text_box_params);
bool line_add_words(Line* line, Iterator* word_iter, LastLineCallback last_line_cb);
typedef struct {
LastLineCallback last_line_cb;
RenderLineCallback render_line_cb;
LayoutUpdateCallback layout_update_cb;
StopConditionCallback stop_condition_cb;
} WalkLinesCallbacks;
#define WALK_LINE_CALLBACKS_EMPTY ((WalkLinesCallbacks){ 0, 0, 0, 0 })

View file

@ -0,0 +1,304 @@
/*
* 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 "text_render.h"
#include "gcontext.h"
#include "graphics.h"
#include "process_state/app_state/app_state.h"
#include "system/passert.h"
#include "text_resources.h"
#include "util/bitset.h"
#include "util/math.h"
#if !defined(__clang__)
#pragma GCC optimize ("O2")
#endif
static GRect get_glyph_rect(const GlyphData* glyph) {
GRect r = {
.size.w = glyph->header.width_px,
.size.h = glyph->header.height_px,
.origin.x = glyph->header.left_offset_px,
.origin.y = glyph->header.top_offset_px
};
return r;
}
/// This function returns the x coordinate of where to write the contents of a given word (32-bits)
/// of data from the 1-bit frame buffer into the 8-bit framebuffer
/// @param dest_bitmap 8-bit destination frame buffer bitmap
/// @param block_addr source address in 1-bit frame buffer of where the word is being updated
/// within a given row; assumed to be zero-based
/// @param y_offset row offset within the source 1-bit frame buffer
T_STATIC int32_t prv_convert_1bit_addr_to_8bit_x(GBitmap *dest_bitmap, uint32_t *block_addr,
int32_t y_offset) {
// Each byte block_addr corresponds to 8 pixels (i.e. 4-bytes in the 8-bit frame buffer).
// Thus multiply by 8 to get the word offset within the destination 8-bit frame buffer.
// Also need to account for the fact that the 1-bit frame buffer has 16 bits of unused space
// on each row (thus 16 bytes need to be subtracted from the destination address since there is
// no padding on each row of the 8-bit frame buffer.
const int32_t padding = (32 - (dest_bitmap->bounds.size.w % 32)) % 32;
// Calculate the overall offset in the 8-bit bitmap
const int32_t bitmap_offset_8bit = ((uint32_t)block_addr * 8) - (padding * y_offset);
// Calculate just the offset from the start of the target row in the 8-bit bitmap (i.e. "x")
return bitmap_offset_8bit - (dest_bitmap->bounds.size.w * y_offset);
}
// PRO TIP: if you have to modify this function, expect to waste the rest of your day on it
void render_glyph(GContext* const ctx, const uint32_t codepoint, FontInfo* const font,
const GRect cursor) {
if (codepoint_is_special(codepoint)) {
TextRenderState *state = app_state_get_text_render_state();
if (state->special_codepoint_handler_cb) {
state->special_codepoint_handler_cb(ctx, codepoint, cursor,
state->special_codepoint_handler_context);
}
return;
}
const GlyphData* glyph = text_resources_get_glyph(&ctx->font_cache, codepoint, font);
PBL_ASSERTN(glyph);
// Bitfiddle the metrics data:
GRect glyph_metrics = get_glyph_rect(glyph);
// Calculate the box that we intend to draw to the screen, in screen coordinates
GRect glyph_target = {
.origin = { .x = cursor.origin.x + glyph_metrics.origin.x,
.y = cursor.origin.y + glyph_metrics.origin.y },
.size = { .w = glyph_metrics.size.w,
.h = glyph_metrics.size.h }
};
// The destination bitmap's x-coordinate and row advance. Used in the loop below.
GBitmap* dest_bitmap = graphics_context_get_bitmap(ctx);
const int32_t x = (int32_t)((int16_t)cursor.origin.x + (int16_t)glyph_metrics.origin.x);
// Now clip that box against the screen/other UI elements. This rect will be the rect that we
// actually fill with bits on the screen.
GRect clipped_glyph_target = glyph_target;
grect_clip(&clipped_glyph_target, &ctx->draw_state.clip_box);
// The number of bits to be clipped off the edges
const int left_clip = clipped_glyph_target.origin.x - glyph_target.origin.x;
const int right_clip = MIN(glyph_target.size.w,
MAX(0, glyph_target.size.w - clipped_glyph_target.size.w - left_clip));
#if SCREEN_COLOR_DEPTH_BITS == 8
// Set base address to 0 for 8-bit as this will be later translated to the destination bitmap
// address - so do all calculations so everything is offset from 0
uint32_t * base_addr = 0;
#else
uint32_t * base_addr = ((uint32_t*)dest_bitmap->addr);
#endif
const uint32_t * const dest_block_x_begin = base_addr +
(left_clip ?
MAX(0, (((x + left_clip + 31)/ 32) - 1)) : (x / 32));
if (clipped_glyph_target.size.h == 0 || clipped_glyph_target.size.w == 0) {
return;
}
#if SCREEN_COLOR_DEPTH_BITS == 8
// NOTE: Since all calculations are based on 1-bit calculation - use the row size from
// the 1-bit frame buffer
const int row_size_bytes = 4 * ((dest_bitmap->bounds.size.w / 32) +
((dest_bitmap->bounds.size.w % 32) ? 1 : 0));
#else
const int row_size_bytes = dest_bitmap->row_size_bytes;
#endif // SCREEN_COLOR_DEPTH_BITS == 8
// Number of blocks (i.e. 32-bit chunks)
const int dest_row_length = row_size_bytes / 4;
// The number of bits between the beginning of dest_block and glyph_block.
// If x is negative we need to be fancy to get the rounded down remainder. This
// is the number of bits to the right of the next 32-bit boundry to the left.
// For example, if x is -5 we want this shift to be 27, since -32 (the nearest
// boundry) + 27 = -5
const uint8_t dest_shift_at_line_begin = (x >= 0) ?
x % 32 :
(x - ((x / 32) * 32));
uint8_t dest_shift = dest_shift_at_line_begin;
// The glyph bitmap starts the block after the metrics data:
uint32_t const* glyph_block = glyph->data;
// Set up the first piece of source glyph bitmap:
int8_t glyph_block_bits_left = 32;
uint32_t src = *glyph_block;
// Use bit-rotate to align to shift the bitmap to align with the destination.
// The advantage of rotate vs. bitwise shift is that we can use
// the bits that wrapped around for the next dest_block
rotl32(src, dest_shift);
int8_t src_rotated = dest_shift;
// how many 32-bit blocks do we need to bitblt on each row. If we're not word aligned we'll need to
// modify an extra partial word, as we'll have an incomplete word on either side of the line segment
// we're modifying.
// For 1-bit, each pixel goes into one bit in dest bitmap - so 32 pixels per block
const uint8_t num_dest_blocks_per_row = (clipped_glyph_target.size.w / 32) +
(((dest_shift + left_clip) % 32) ? 1 : 0);
// Handle clipping at the top of the character. We need to skip a number of bits in our source data.
const unsigned int bits_to_skip = glyph_metrics.size.w * (clipped_glyph_target.origin.y - glyph_target.origin.y);
if (bits_to_skip) {
glyph_block += bits_to_skip / 32;
src = *glyph_block;
// Simulate the rotate that happens at the bottom of the bitblt loop so our source value is set
// up just as if we actually rendered those first few lines.
rotl32(src, (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32);
src_rotated = (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32;
glyph_block_bits_left -= bits_to_skip % 32;
}
for (int dest_y = clipped_glyph_target.origin.y; dest_y != clipped_glyph_target.origin.y + clipped_glyph_target.size.h; ++dest_y) {
dest_shift = dest_shift_at_line_begin;
// Number of bits to render on this line.
uint8_t glyph_line_bits_left = clipped_glyph_target.size.w;
uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length);
const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row + 1;
if (left_clip) {
const int left_clip_shift = left_clip % 32;
const int clipped_blocks = left_clip / 32;
dest_shift = (dest_shift + left_clip_shift) % 32;
glyph_block_bits_left -= left_clip_shift;
glyph_block += clipped_blocks;
if (glyph_block_bits_left <= 0) {
src = *(++glyph_block);
glyph_block_bits_left += 32;
// Need to account for the dest_shift when loading up the new glyph block
rotl32(src, glyph_block_bits_left + dest_shift);
src_rotated = glyph_block_bits_left + dest_shift;
}
dest_block += clipped_blocks;
}
while (dest_block != dest_block_end && glyph_line_bits_left) {
PBL_ASSERT(dest_block < dest_block_end, "DB=<%p> DBE=<%p>", dest_block, dest_block_end);
PBL_ASSERTN(dest_block >= (uint32_t*) base_addr);
PBL_ASSERTN(dest_block < (uint32_t*) base_addr + row_size_bytes *
(dest_bitmap->bounds.origin.y + dest_bitmap->bounds.size.h));
// bitblt part of glyph_block:
const uint8_t number_of_bits = MIN(32 - dest_shift, MIN(glyph_line_bits_left, glyph_block_bits_left));
const uint32_t mask = (((1 << number_of_bits) - 1) << dest_shift);
#if SCREEN_COLOR_DEPTH_BITS == 8
// dest_block points to the block if the dest image was a 1-bit buffer
// translate this to an x coordinate in the 8-bit buffer
const int32_t block_start_x = prv_convert_1bit_addr_to_8bit_x(dest_bitmap, dest_block,
dest_y);
const GBitmapDataRowInfo data_row = gbitmap_get_data_row_info(dest_bitmap, dest_y);
// Only enter the loop if the current block is within the valid data row range
if (block_start_x + 31 >= data_row.min_x && block_start_x <= data_row.max_x) {
uint8_t *dest_addr = data_row.data + block_start_x;
// For each bit in block, write that bit to the dest_bitmap
for (unsigned int bitindex = 0; bitindex < 32; bitindex++) {
const int32_t current_x = block_start_x + bitindex;
// Stop iteration early if we have reached the end of the data row
if (current_x > data_row.max_x) {
break;
}
// Skip over pixels outside of the bitmap data's x coordinate range
if (current_x < data_row.min_x) {
continue;
}
// Find position in dest_bitmap that corresponds to the bit index
// Write to that position if mask for that bit is 1
if ((mask & src) & (1 << bitindex)) {
GColor dest_color;
if (ctx->draw_state.compositing_mode == GCompOpSet) {
// Blend (i.e. for transparency) if GCompOpSet
dest_color = gcolor_alpha_blend(ctx->draw_state.text_color,
(GColor) {.argb = dest_addr[bitindex]});
} else {
dest_color = ctx->draw_state.text_color;
dest_color.a = 3;
}
dest_addr[bitindex] = dest_color.argb;
}
}
}
#else
if (gcolor_equal(ctx->draw_state.text_color, GColorBlack)) {
*(dest_block) &= ~(mask & src);
} else {
*(dest_block) |= mask & src;
}
#endif
dest_shift = (dest_shift + number_of_bits) % 32;
glyph_block_bits_left -= number_of_bits;
glyph_line_bits_left -= number_of_bits;
if (glyph_block_bits_left <= 0) {
// We ran out of bits in the current glyph block. Get the next glyph blob:
src = *(++glyph_block);
glyph_block_bits_left += 32;
rotl32(src, dest_shift);
src_rotated = dest_shift;
// Continue with this dest_block if there is still space left:
if (dest_shift) {
continue;
}
}
++dest_block;
}
dest_shift += right_clip % 32;
// emulate having drawn the right clip
if (glyph_block_bits_left <= right_clip) {
int jump_words = (right_clip - glyph_block_bits_left) / 32 + 1;
glyph_block += jump_words;
src = *glyph_block;
rotl32(src, src_rotated);
glyph_block_bits_left += 32 * jump_words;
}
glyph_block_bits_left -= right_clip;
// Rotate the bits into the right position for the next row:
dest_shift = dest_shift_at_line_begin - dest_shift;
rotl32(src, dest_shift % 32);
src_rotated = (src_rotated + dest_shift) % 32;
}
graphics_context_mark_dirty_rect(ctx, clipped_glyph_target);
}
void text_render_set_special_codepoint_cb(SpecialCodepointHandlerCb handler, void *context) {
TextRenderState *state = app_state_get_text_render_state();
state->special_codepoint_handler_cb = handler;
state->special_codepoint_handler_context = context;
}

View file

@ -0,0 +1,38 @@
/*
* 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.
*/
#pragma once
#include "applib/fonts/codepoint.h"
#include "applib/fonts/fonts_private.h"
#include "applib/graphics/gtypes.h"
#include "gtypes.h"
#include <inttypes.h>
typedef struct GContext GContext;
typedef void (*SpecialCodepointHandlerCb)(GContext *ctx, Codepoint codepoint, GRect cursor,
void *context);
void render_glyph(GContext* const ctx, const uint32_t codepoint, FontInfo* const font,
const GRect cursor);
// This function sets a handler callback for handling special codepoints encountered during text
// rendering. This allows special draw operations at the cursor position that the codepoint occurs.
// This must be set to NULL when the window using it goes out of focus.
void text_render_set_special_codepoint_cb(SpecialCodepointHandlerCb handler, void *context);

View file

@ -0,0 +1,653 @@
/*
* 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 "text.h"
#include "text_resources.h"
#include "syscall/syscall.h"
#include "applib/fonts/fonts.h"
#include "applib/fonts/fonts_private.h"
#include "resource/resource_ids.auto.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/profiler.h"
#include "util/math.h"
#include "util/size.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#define RLE4_UNITS_BIT_WIDTH (4)
#define RLE4_UNITS_PER_BYTE (8 / RLE4_UNITS_BIT_WIDTH)
static const size_t s_font_md_size[] = {
0, // There currently is no font version 0. This makes decoding much easier & consistent
sizeof(FontMetaDataV1),
sizeof(FontMetaData),
sizeof(FontMetaDataV3)
};
static uint8_t prv_font_hash(Codepoint codepoint, uint8_t table_size) {
return (codepoint % table_size);
}
static Codepoint prv_offset_table_get_codepoint(FontCache *font_cache, const FontMetaData *md,
int index) {
const bool offset_16 = HAS_FEATURE(md->version, VERSION_FIELD_FEATURE_OFFSET_16);
if (md->codepoint_bytes == 2) {
return offset_16 ? font_cache->offsets_buffer_2_2[index].codepoint :
font_cache->offsets_buffer_2_4[index].codepoint;
} else {
return offset_16 ? font_cache->offsets_buffer_4_2[index].codepoint :
font_cache->offsets_buffer_4_4[index].codepoint;
}
}
static uint32_t prv_offset_table_get_offset(FontCache *font_cache, const FontMetaData *md,
int index) {
const bool offset_16 = HAS_FEATURE(md->version, VERSION_FIELD_FEATURE_OFFSET_16);
if (md->codepoint_bytes == 2) {
return offset_16 ? font_cache->offsets_buffer_2_2[index].offset :
font_cache->offsets_buffer_2_4[index].offset;
} else {
return offset_16 ? font_cache->offsets_buffer_4_2[index].offset :
font_cache->offsets_buffer_4_4[index].offset;
}
}
static uint32_t prv_offset_table_entry_size(const FontMetaData *md) {
const bool offset_16 = HAS_FEATURE(md->version, VERSION_FIELD_FEATURE_OFFSET_16);
if (md->codepoint_bytes == 2) {
return offset_16 ? sizeof(OffsetTableEntry_2_2) : sizeof(OffsetTableEntry_2_4);
} else {
return offset_16 ? sizeof(OffsetTableEntry_4_2) : sizeof(OffsetTableEntry_4_4);
}
}
static int prv_offset_table_get_id(const FontMetaData *md, Codepoint codepoint) {
if (FONT_VERSION(md->version) == FONT_VERSION_1) {
return (1);
} else {
return prv_font_hash(codepoint, md->hash_table_size);
}
}
static int prv_load_offset_table(Codepoint codepoint, FontCache *font_cache,
const FontResource *font_res) {
const int table_id = prv_offset_table_get_id(&font_res->md, codepoint);
if (table_id == font_cache->offset_table_id) {
return font_cache->offset_table_size;
}
size_t num_bytes, offset, num_entries;
const uint8_t version = FONT_VERSION(font_res->md.version);
if (version == FONT_VERSION_1) {
offset = s_font_md_size[FONT_VERSION_1];
num_bytes = font_res->md.number_of_glyphs * prv_offset_table_entry_size(&font_res->md);
num_entries = font_res->md.number_of_glyphs;
PBL_ASSERTN(num_bytes <= sizeof(font_cache->offsets_buffer_2_2));
} else {
FontHashTableEntry table_entry;
Codepoint hash_entry_offset = s_font_md_size[version] + table_id * sizeof(FontHashTableEntry);
// find which bucket the codepoint was put into TODO: cache hash table?
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG,
"HTE read: table_id:%d, cp:%"PRIx32", offset:%"PRIx32,
table_id, codepoint, hash_entry_offset);
SYS_PROFILER_NODE_START(text_render_flash);
sys_resource_load_range(font_res->app_num, font_res->resource_id, hash_entry_offset,
(uint8_t*)&table_entry, sizeof(FontHashTableEntry));
SYS_PROFILER_NODE_STOP(text_render_flash);
offset = s_font_md_size[version] +
(sizeof(FontHashTableEntry) * font_res->md.hash_table_size) + table_entry.offset;
num_bytes = table_entry.count * prv_offset_table_entry_size(&font_res->md);
num_entries = table_entry.count;
PBL_ASSERTN(num_bytes <= sizeof(font_cache->offsets_buffer_4_4));
}
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "HT read: offset: %zx, bytes: %zu", offset,
num_bytes);
SYS_PROFILER_NODE_START(text_render_flash);
sys_resource_load_range(font_res->app_num, font_res->resource_id, offset,
(uint8_t *)font_cache->offsets_buffer_4_4, num_bytes);
SYS_PROFILER_NODE_STOP(text_render_flash);
font_cache->offset_table_id = table_id;
font_cache->offset_table_size = num_entries;
return num_entries;
}
static uint32_t prv_get_cache_key(const FontResource *font_res, Codepoint codepoint) {
// Ideally we'd be able to use the full app_num, resource_id and codepoint combined into a key
// in a unique matter, but unfortunately there aren't enough bits. Note that this value needs to
// be unique, there's no collision handling and if one does occur you'll just end up reading the
// wrong metadata. Luckily we don't need to store all the bits due to assumptions we can make.
// We know that for a given FontCache we'll only use a combination of fonts from the running app
// and system fonts and we'll never use custom fonts from two different app banks at the same
// time. This means we only need a single bit to store whether the font is for the system bank
// (bank 0) or an app bank (bank > 0).
// resource_id is technically a full 32-bit id but in practice it's much smaller. The firmware
// only uses 400~ unique resources at the time of writing so 14 bits (16384 resources) should be
// enough.
// Therefore our key layout becomes:
// is_app:1
// resource_id:14
// codepoint:17
const bool is_app = (font_res->app_num != 0);
return (is_app ? 1 << 31 : 0) |
((font_res->resource_id << 17) |
(codepoint & 0x0001FFFF));
}
static uint32_t prv_get_glyph_table_offset(FontCache *font_cache, Codepoint codepoint,
const FontResource *font_res) {
int min_idx = 0;
int max_idx = prv_load_offset_table(codepoint, font_cache, font_res);
uint32_t offset = 0;
while (max_idx >= min_idx) {
int mid_idx = (max_idx + min_idx) / 2;
Codepoint codepoint_at_mid_idx = prv_offset_table_get_codepoint(font_cache, &font_res->md,
mid_idx);
if (codepoint_at_mid_idx < codepoint) {
min_idx = mid_idx + 1;
} else if (codepoint_at_mid_idx > codepoint) {
max_idx = mid_idx - 1;
} else {
offset = prv_offset_table_get_offset(font_cache, &font_res->md, mid_idx);
break;
}
}
return offset;
}
static uint32_t prv_get_glyph_data_offset(Codepoint codepoint, FontCache *font_cache,
const FontResource *font_res) {
const uint32_t offset = prv_get_glyph_table_offset(font_cache, codepoint, font_res);
if (offset <= 0) {
return 0;
}
// Compute the offset of the glyph data (relative to the beginning
// of the font blob).
//
// See: https://pebbletechnology.atlassian.net/wiki/display/DEV/Pebble+Resource+Pack+Format
uint32_t address;
const uint32_t version = FONT_VERSION(font_res->md.version);
if (version == FONT_VERSION_1) {
address = s_font_md_size[FONT_VERSION_1] +
(font_res->md.number_of_glyphs * prv_offset_table_entry_size(&font_res->md)) +
(sizeof(uint32_t) * offset);
} else {
address = s_font_md_size[version] +
(sizeof(FontHashTableEntry) * font_res->md.hash_table_size) +
(prv_offset_table_entry_size(&font_res->md) * font_res->md.number_of_glyphs) +
offset;
}
return address;
}
//! Decode RLE4 decoded glyph data in place.
//! @return g or NULL on error.
#define RLE4_SYMBOL_MASK 0x08
#define RLE4_LENGTH_MASK 0x07
static GlyphData *prv_decompress_glyph_data(GlyphData *g, uint8_t *src) {
// This RLE4 decompressor expects GlyphData to be formatted like this:
// [ <header> | <free space> | <encoded glyph> ]
// *src is a pointer to the beginning of <encoded glyph>
//
// Once decompressed, Glyph Data will be formatted like this:
// [ <header> | <decoded glyph> | <free space & remenants of encoded glyph data> ]
//
// The glyph is decoded in-place, so obviously, it's imperative that <decoded glyph> and
// <encoded glyph> not overlap at any time. This is checked by fontgen.py and confirmed
// by the code below.
//
// RLE4 data is encoded as a stream of RLE Units.
// 0 1 2 3
// +-+-+-+-+
// |*| Len | Where * is the encoded symbol [0,1] and 'Len + 1' is the number of symbols
// +-+-+-+-+ in the run [1,8]. For example, 1000 expands to '1' and 0100 expands to '00000'
//
// RLE Units are packed as pairs -- two to a byte.
//
// Decoding is done by expanding the bit patterns into a buffer until we have at least 8 bits of
// pixels to write.
PBL_ASSERTN(g);
PBL_ASSERTN(src > (uint8_t *)g->data);
uint8_t *dst = (uint8_t *)g->data;
unsigned total_pixels_decoded = 0;
unsigned num_rle_units = g->header.num_rle_units;
// Decoded pixel buffer. At least 16 bits (to hold 2 decoded RLE4s)
uint16_t buf = 0;
int8_t buf_num_bits = 0;
PBL_ASSERTN(num_rle_units <= (CACHE_GLYPH_SIZE * RLE4_UNITS_PER_BYTE));
while (num_rle_units) {
PBL_ASSERTN(src < &((uint8_t *)g->data)[CACHE_GLYPH_SIZE]);
uint8_t rle_unit_pair = *src++;
for (unsigned int i = 0; i < RLE4_UNITS_PER_BYTE; ++i) {
if (!num_rle_units) {
break; // Handle a padded, odd number of rle units
}
// Number of bits in this run
uint8_t length = (rle_unit_pair & RLE4_LENGTH_MASK) + 1;
// Symbol of this run. We don't need to generate a pattern of 0s. ;-)
if (rle_unit_pair & RLE4_SYMBOL_MASK) {
uint8_t pattern = ((1 << length) - 1); // List of 'length' 1s
buf |= (pattern << buf_num_bits);
}
buf_num_bits += length;
total_pixels_decoded += length;
// Store 8 bits worth of pixels
if (buf_num_bits >= 8) {
PBL_ASSERTN(dst < src);
*dst++ = (buf & 0xFF);
buf >>= 8;
buf_num_bits -= 8;
}
// Now process the second nibble
rle_unit_pair >>= 4;
num_rle_units--;
}
}
// Flush out any remaining pixels
while (buf_num_bits > 0) {
PBL_ASSERTN(dst < &((uint8_t *)g->data)[CACHE_GLYPH_SIZE]);
*dst++ = (buf & 0xFF);
buf >>= 8;
buf_num_bits -= 8;
}
// Fix-up the height to reflect the bit pattern instead of the number of RLE units.
if (g->header.width_px) {
g->header.height_px = total_pixels_decoded / g->header.width_px;
}
return g;
}
static bool prv_load_glyph_bitmap(Codepoint codepoint, const FontResource *font_res,
LineCacheData *data) {
GlyphData *g = &data->glyph_data;
const size_t bitmap_offset = (FONT_VERSION(font_res->md.version) == FONT_VERSION_1) ?
sizeof(GlyphHeaderDataV1) : sizeof(GlyphHeaderData);
const uint32_t bitmap_addr = data->resource_offset + bitmap_offset;
size_t glyph_size_bytes;
// Handle RLE4 compressed glyphs. header.height_px has been 'borrowed' to mean the number of
// 4-bit RLE units used to encode the glyph. We determine the height by decoding the number of
// bits and then dividing by the width. glyph.height_px must be updated!
if (HAS_FEATURE(font_res->md.version, VERSION_FIELD_FEATURE_RLE4)) {
// Two RLE4 units per byte. Round up to the next whole byte
glyph_size_bytes = (g->header.height_px + (RLE4_UNITS_PER_BYTE - 1)) / RLE4_UNITS_PER_BYTE;
} else {
// Number of bytes, make sure we round up to the next whole byte
glyph_size_bytes = ((g->header.width_px * g->header.height_px) + (8 - 1)) / 8;
}
PBL_ASSERT(glyph_size_bytes <= CACHE_GLYPH_SIZE,
"text codepoint %"PRIx32" is %zu bytes, overflowing %zu max size", codepoint,
glyph_size_bytes, CACHE_GLYPH_SIZE);
if (glyph_size_bytes) {
uint8_t *target;
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG,
"GD read: cp: %"PRIx32", res_bank: %"PRIu32", res_id: %"PRIu32", "
"offset: %"PRIx32", bytes: %zu",
codepoint, font_res->app_num, font_res->resource_id, bitmap_addr, glyph_size_bytes);
if (HAS_FEATURE(font_res->md.version, VERSION_FIELD_FEATURE_RLE4)) {
// Load the glyph data at the end of the buffer
target = &((uint8_t *)g->data)[CACHE_GLYPH_SIZE - glyph_size_bytes];
} else {
target = (uint8_t *)g->data;
}
SYS_PROFILER_NODE_START(text_render_flash);
size_t num_bytes_loaded = sys_resource_load_range(font_res->app_num, font_res->resource_id,
bitmap_addr, target, glyph_size_bytes);
SYS_PROFILER_NODE_STOP(text_render_flash);
if (glyph_size_bytes && !num_bytes_loaded) {
PBL_LOG(LOG_LEVEL_WARNING,
"Failed to load glyph bitmap from resources; cp: %"PRIx32", addr: %"PRIx32,
codepoint, bitmap_addr);
return false;
}
if (HAS_FEATURE(font_res->md.version, VERSION_FIELD_FEATURE_RLE4)) {
SYS_PROFILER_NODE_START(text_render_compress);
g = prv_decompress_glyph_data(g, target);
SYS_PROFILER_NODE_STOP(text_render_compress);
}
}
data->is_bitmap_loaded = true;
return true;
}
static const GlyphData *prv_get_glyph_metadata_from_spi(Codepoint codepoint,
FontCache *font_cache,
const FontResource *font_res,
bool need_bitmap) {
const uint32_t cache_key = prv_get_cache_key(font_res, codepoint);
LineCacheData *cached = NULL;
// If we don't have bitmap caching, we have a single glyph_buffer that contains the last used
// glyph. If this matches the glyph we're looking for right now, that's what we want to use.
// Potentially this also has the bitmap loaded already.
#if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING
if (font_cache->glyph_buffer_key == cache_key) {
cached = (LineCacheData *)(font_cache->glyph_buffer);
}
#endif
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "looking up cp: %"PRIx32", key:%"PRIx32,
codepoint, cache_key);
// If the glyph_buffer doesn't match this glyph, or we have bitmap caching, check the
// keyed_circular_cache for this glyph.
if (!cached) {
cached = keyed_circular_cache_get(&font_cache->line_cache, cache_key);
#if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING
// If we don't have bitmap caching, the keyed_circular_cache entry cannot store the bitmap.
// Therefore, we need to copy the matched entry into `glyph_buffer` which does have the space
// to store the bitmap.
if (cached) {
memcpy(font_cache->glyph_buffer, cached, sizeof(LineCacheData));
font_cache->glyph_buffer_key = cache_key;
// Point `cached` at the glyph buffer.
cached = (LineCacheData *)(font_cache->glyph_buffer);
cached->is_bitmap_loaded = false;
}
#endif
}
if (cached) {
if (cached->resource_offset == 0) {
// missing character
return NULL;
}
if (need_bitmap &&
!cached->is_bitmap_loaded &&
!prv_load_glyph_bitmap(codepoint, font_res, cached)) {
return NULL;
}
return &cached->glyph_data;
}
// We missed the cache, so we need to build a new cache entry.
LineCacheData *data = &font_cache->cache_data_scratch;
data->is_bitmap_loaded = false;
data->resource_offset = prv_get_glyph_data_offset(codepoint, font_cache, font_res);
GlyphData *g = &data->glyph_data;
if (data->resource_offset == 0) {
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "offset for cp: %"PRIx32" is NULL", codepoint);
// Put the missing character into our cache so we don't waste time looking for it again
keyed_circular_cache_push(&font_cache->line_cache, cache_key, data);
return NULL;
}
size_t num_bytes_loaded;
if (FONT_VERSION(font_res->md.version) == FONT_VERSION_1) {
GlyphHeaderDataV1 header;
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "LGMD READ: offset: %"PRIx32", bytes: %zu",
data->resource_offset, sizeof(header));
SYS_PROFILER_NODE_START(text_render_flash);
num_bytes_loaded = sys_resource_load_range(font_res->app_num, font_res->resource_id,
data->resource_offset, (uint8_t *)&header,
sizeof(header));
SYS_PROFILER_NODE_STOP(text_render_flash);
// convert to a GlyphHeaderData struct
memcpy(&g->header, &header, sizeof(GlyphHeaderData));
g->header.horiz_advance = header.horiz_advance;
} else {
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG,
"GMD read: cp: %"PRIx32", offset: %"PRId32", bytes: %zu", codepoint,
data->resource_offset, sizeof(GlyphHeaderData));
SYS_PROFILER_NODE_START(text_render_flash);
num_bytes_loaded = sys_resource_load_range(font_res->app_num, font_res->resource_id,
data->resource_offset, (uint8_t *)&g->header,
sizeof(GlyphHeaderData));
SYS_PROFILER_NODE_STOP(text_render_flash);
}
if (!num_bytes_loaded) {
PBL_LOG(LOG_LEVEL_WARNING,
"Failed to load glyph metadata from resources; cp: %"PRIx32", offset: %"PRIx32,
codepoint, data->resource_offset);
return NULL;
}
LineCacheData *final_data;
#if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING
// Copy the info into the glyph_buffer.
// This must be done _before_ loading the bitmap, otherwise loading the bitmap may modify the
// metadata! We will use `glyph_buffer` as the final data, and leave `data` as the uncooked
// version, that way we can push `data` into the circular cache.
memcpy(font_cache->glyph_buffer, data, sizeof(LineCacheData));
font_cache->glyph_buffer_key = cache_key;
final_data = (LineCacheData *)(font_cache->glyph_buffer);
#else
final_data = data;
#endif
if (need_bitmap &&
!prv_load_glyph_bitmap(codepoint, font_res, final_data)) {
return NULL;
}
// We push `data`, which will be cooked data if the bitmap is stored along with it, or
// the uncooked data if it's not. In reality, this only matters to compressed glyphs, since
// compressed glyphs are the only case where the metadata gets modified.
// The only time the data is cooked is when loading a bitmap and the glyph is compressed, in
// which case the `num_rle_units` field is turned back into `height_px`.
keyed_circular_cache_push(&font_cache->line_cache, cache_key, data);
// We return `final_data` though, because that has the actual metadata info that needs to be
// used.
return &final_data->glyph_data;
}
static void prv_check_font_cache(FontCache *font_cache, const FontResource *font_res) {
// Invalidate the offset table
if (font_cache->cached_font != font_res) {
font_cache->offset_table_id = -1;
font_cache->cached_font = font_res;
}
}
static bool prv_load_font_res(ResAppNum app_num, uint32_t resource_id, FontResource *font_res,
bool is_extended) {
font_res->resource_id = resource_id;
font_res->app_num = app_num;
if (resource_id != RESOURCE_ID_FONT_FALLBACK_INTERNAL &&
!sys_resource_is_valid(app_num, resource_id)) {
if (!is_extended) {
PBL_LOG(LOG_LEVEL_WARNING, "Invalid text resource id %"PRId32, resource_id);
}
return false;
}
if (app_num == SYSTEM_APP && !sys_resource_get_and_cache(app_num, resource_id)) {
return false;
}
PBL_LOG_D(LOG_DOMAIN_TEXT, LOG_LEVEL_DEBUG, "FMD read: bytes:%d", (int)sizeof(FontMetaDataV3));
FontMetaDataV3 header;
SYS_PROFILER_NODE_START(text_render_flash);
uint32_t bytes_read = sys_resource_load_range(app_num, resource_id, 0,
(uint8_t*)&header, sizeof(FontMetaDataV3));
SYS_PROFILER_NODE_STOP(text_render_flash);
if (bytes_read != sizeof(FontMetaDataV3)) {
PBL_LOG(LOG_LEVEL_ERROR, "Tried to load resource too small to have metadata for res %"PRId32,
resource_id);
return false;
}
memcpy(&font_res->md, &header, sizeof(FontMetaData));
switch (header.version) {
case FONT_VERSION_1:
// no hash table, no variable codepoint size, no feature bits
font_res->md.hash_table_size = 0;
// Version 1 fonts do use 16 bit offsets and 16 bit codepoints. This simplifies the code above
font_res->md.codepoint_bytes = 2;
font_res->md.version |= VERSION_FIELD_FEATURE_OFFSET_16;
break;
case FONT_VERSION_2:
break;
case FONT_VERSION_3:
// Make sure that the font header is internally consistent
PBL_ASSERTN(header.size == sizeof(FontMetaDataV3));
// HACK alert: Copy the feature bits to the top two bits of the header version.
if (header.features & FEATURE_OFFSET_16) {
font_res->md.version |= VERSION_FIELD_FEATURE_OFFSET_16;
}
if (header.features & FEATURE_RLE4) {
font_res->md.version |= VERSION_FIELD_FEATURE_RLE4;
}
break;
default:
PBL_LOG(LOG_LEVEL_ERROR, "Unknown font resource version %"PRIu8, header.version);
return false;
}
return true;
}
static const FontResource *prv_font_res_for_codepoint(Codepoint codepoint,
const FontInfo *font_info) {
if (!codepoint_is_latin(codepoint) &&
!codepoint_is_emoji(codepoint) &&
!codepoint_is_special(codepoint) &&
font_info->extended) {
// Latin & emoji codepoints are in base, others are in extension
return (&font_info->extension);
} else if (codepoint_is_emoji(codepoint) &&
font_info->base.app_num == SYSTEM_APP) {
// Assuming we are using base
FontInfo *emoji_font = fonts_get_system_emoji_font_for_size(font_info->max_height);
if (emoji_font) {
return &emoji_font->base;
}
}
return (&font_info->base);
}
static void prv_resource_changed_callback(void *data) {
FontInfo *font_info = (FontInfo *)data;
font_info->loaded = false;
font_info->extended = false;
}
///////////////////////////
// Public API
bool text_resources_init_font(ResAppNum app_num, uint32_t font_resource,
uint32_t extended_resource, FontInfo *font_info) {
// load the base of the font or bail
if (!font_resource ||
!prv_load_font_res(app_num, font_resource, &font_info->base, false /* is_extended */)) {
return false;
}
// look for an extension font and load it
if (extended_resource) {
// if you want 3rd party apps to use extended fonts, you'll have to unwatch when they unload
// and create a syscall for resource_watch
PBL_ASSERTN(app_num == SYSTEM_APP);
if (font_info->extension_changed_cb == NULL) {
font_info->extension_changed_cb = resource_watch(app_num, extended_resource,
prv_resource_changed_callback, font_info);
}
font_info->extended = prv_load_font_res(app_num, extended_resource, &font_info->extension,
true /* is_extended */);
}
font_info->max_height = MAX(font_info->extension.md.max_height, font_info->base.md.max_height);
font_info->loaded = true;
return true;
}
static const GlyphData *prv_get_glyph(FontCache *font_cache, Codepoint codepoint,
FontInfo *font_info, bool need_bitmap) {
if (!font_info->loaded) {
sys_font_reload_font(font_info);
}
// if we cannot find the codepoint we are looking for, we should always be
// able to find the wildcard (square box) or ' ' character to display. We use
// the wildcard codepoint from the base font in case the extension pack has
// been deleted
const Codepoint codepoint_list[] = { codepoint, font_info->base.md.wildcard_codepoint, ' ' };
for (unsigned int i = 0; i < ARRAY_LENGTH(codepoint_list); i++) {
const FontResource *font_res = prv_font_res_for_codepoint(codepoint_list[i], font_info);
prv_check_font_cache(font_cache, font_res);
const GlyphData *data = prv_get_glyph_metadata_from_spi(codepoint_list[i], font_cache,
font_res, need_bitmap);
if (data) {
return data;
}
}
PBL_LOG(LOG_LEVEL_WARNING, "failed to load glyph or wildcard");
return NULL;
}
int8_t text_resources_get_glyph_horiz_advance(FontCache *font_cache, const Codepoint codepoint,
FontInfo *font_info) {
const GlyphData *g = prv_get_glyph(font_cache, codepoint, font_info, false /* need_bitmap */);
if (!g) {
return 0;
}
return g->header.horiz_advance;
}
const GlyphData *text_resources_get_glyph(FontCache *font_cache, const Codepoint codepoint,
FontInfo *font_info) {
return prv_get_glyph(font_cache, codepoint, font_info, true /* need_bitmap */);
}

View file

@ -0,0 +1,151 @@
/*
* 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.
*/
#pragma once
#include "applib/fonts/fonts_private.h"
#include "applib/fonts/codepoint.h"
#include "util/keyed_circular_cache.h"
#include <stdint.h>
typedef struct __attribute__((__packed__)) {
uint8_t width_px;
union {
uint8_t height_px;
uint8_t num_rle_units;
};
int8_t left_offset_px;
int8_t top_offset_px;
int8_t horiz_advance;
} GlyphHeaderData;
typedef struct __attribute__((__packed__)) {
uint8_t width_px;
uint8_t height_px;
int8_t left_offset_px;
int8_t top_offset_px;
uint8_t empty[3];
int8_t horiz_advance;
} GlyphHeaderDataV1;
typedef struct __attribute__((__packed__)) {
GlyphHeaderData header;
uint32_t data[];
} GlyphData;
//! Maps a codepoint to the location of the actual font data.
typedef struct __attribute__((__packed__)) {
Codepoint codepoint : 16;
uint16_t offset;
} OffsetTableEntry_2_2;
typedef struct __attribute__((__packed__)) {
Codepoint codepoint : 16;
uint32_t offset;
} OffsetTableEntry_2_4;
typedef struct __attribute__((__packed__)) {
Codepoint codepoint;
uint32_t offset;
} OffsetTableEntry_4_4;
typedef struct __attribute__((__packed__)) {
Codepoint codepoint;
uint16_t offset;
} OffsetTableEntry_4_2;
#if !defined(MAX_FONT_GLYPH_SIZE)
#define MAX_FONT_GLYPH_SIZE 256
#endif
// Slightly bigger than the biggest glyph we have
// This is the size in bytes for the glyph bitmap data.
#define CACHE_GLYPH_SIZE MAX_FONT_GLYPH_SIZE
typedef struct {
uint32_t resource_offset;
//! Whether the bitmap data in this structure is valid.
bool is_bitmap_loaded;
union {
#if CAPABILITY_HAS_GLYPH_BITMAP_CACHING
//! Glyph data including bitmap
struct __attribute__((__packed__)) {
GlyphHeaderData header_data;
uint8_t data[CACHE_GLYPH_SIZE];
};
#else
//! Glyph data without a bitmap
GlyphHeaderData header_data;
#endif
GlyphData glyph_data;
};
} LineCacheData;
#define LINE_CACHE_SIZE 30
// Allow 1K max for offset tables
#define OFFSET_TABLE_MAX_SIZE (1024)
typedef struct FontCache {
int offset_table_id;
uint16_t offset_table_size;
//! The currently loaded font's offset table.
//! @note this needs to be able to accomodate legacy fonts
union {
OffsetTableEntry_2_2 offsets_buffer_2_2[OFFSET_TABLE_MAX_SIZE / sizeof(OffsetTableEntry_2_2)];
OffsetTableEntry_2_4 offsets_buffer_2_4[OFFSET_TABLE_MAX_SIZE / sizeof(OffsetTableEntry_2_4)];
OffsetTableEntry_4_2 offsets_buffer_4_2[OFFSET_TABLE_MAX_SIZE / sizeof(OffsetTableEntry_4_2)];
OffsetTableEntry_4_4 offsets_buffer_4_4[OFFSET_TABLE_MAX_SIZE / sizeof(OffsetTableEntry_4_4)];
};
//! line_cache's backing storage for keys
KeyedCircularCacheKey cache_keys[LINE_CACHE_SIZE];
//! line_cache's backing storage for data
LineCacheData cache_data[LINE_CACHE_SIZE];
//! some scratch space so we don't need to create a LineCacheData on the stack
LineCacheData cache_data_scratch;
// Since we don't have bitmap caching, we need to have somewhere to store the bitmap data.
#if !CAPABILITY_HAS_GLYPH_BITMAP_CACHING
//! cache_key for the last used glyph
uint32_t glyph_buffer_key;
//! data for the last used glyph
uint8_t glyph_buffer[sizeof(LineCacheData) + CACHE_GLYPH_SIZE];
#endif
KeyedCircularCache line_cache;
const FontResource *cached_font;
} FontCache;
const GlyphData *text_resources_get_glyph(FontCache *font_cache, Codepoint codepoint,
FontInfo *font_info);
int8_t text_resources_get_glyph_horiz_advance(FontCache *font_cache, Codepoint codepoint,
FontInfo *font_info);
//! Initialize a FontInfo struct with resource contents
//! A FontInfo contains references to up to *two* font resources: a "base" font and an "extension".
//! The base font is part of the system resources pack and contains latin characters and emoji
//! The extension font contains additional characters required to display localized UI or
//! notifications.
//! @note the extension may not be installed and may be removed at any give time
//! @param app_num the ResAppNum associated with this font (i.e. system or app?)
//! @param font_resource the "base" resource id
//! @param extension_resource the "extension" resource id
//! @fontinfo a pointer to the fontinfo struct to initialize
bool text_resources_init_font(ResAppNum app_num, uint32_t font_resource,
uint32_t extension_resource, FontInfo *font_info);