mirror of
https://github.com/google/pebble.git
synced 2025-06-02 08:23:10 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
194
src/fw/applib/graphics/1_bit/bitblt_private.c
Normal file
194
src/fw/applib/graphics/1_bit/bitblt_private.c
Normal 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;
|
||||
}
|
||||
}
|
61
src/fw/applib/graphics/1_bit/framebuffer.c
Normal file
61
src/fw/applib/graphics/1_bit/framebuffer.c
Normal 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;
|
||||
}
|
32
src/fw/applib/graphics/1_bit/framebuffer.h
Normal file
32
src/fw/applib/graphics/1_bit/framebuffer.h
Normal 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);
|
472
src/fw/applib/graphics/8_bit/bitblt_private.c
Normal file
472
src/fw/applib/graphics/8_bit/bitblt_private.c
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
76
src/fw/applib/graphics/8_bit/framebuffer.c
Normal file
76
src/fw/applib/graphics/8_bit/framebuffer.c
Normal 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;
|
||||
}
|
47
src/fw/applib/graphics/8_bit/framebuffer.h
Normal file
47
src/fw/applib/graphics/8_bit/framebuffer.h
Normal 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);
|
52
src/fw/applib/graphics/assets_temp/pug.h
Normal file
52
src/fw/applib/graphics/assets_temp/pug.h
Normal 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,
|
||||
};
|
165
src/fw/applib/graphics/bitblt.c
Normal file
165
src/fw/applib/graphics/bitblt.c
Normal 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));
|
||||
}
|
28
src/fw/applib/graphics/bitblt.h
Normal file
28
src/fw/applib/graphics/bitblt.h
Normal 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);
|
35
src/fw/applib/graphics/bitblt_private.h
Normal file
35
src/fw/applib/graphics/bitblt_private.h
Normal 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[];
|
66
src/fw/applib/graphics/framebuffer.c
Normal file
66
src/fw/applib/graphics/framebuffer.c
Normal 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;
|
||||
}
|
64
src/fw/applib/graphics/framebuffer.h
Normal file
64
src/fw/applib/graphics/framebuffer.h
Normal 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);
|
599
src/fw/applib/graphics/gbitmap.c
Normal file
599
src/fw/applib/graphics/gbitmap.c
Normal 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);
|
||||
}
|
97
src/fw/applib/graphics/gbitmap_pbi.h
Normal file
97
src/fw/applib/graphics/gbitmap_pbi.h
Normal 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)`.
|
||||
//! 
|
||||
//! 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.
|
||||
//! 
|
||||
//!
|
||||
//! - \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;
|
293
src/fw/applib/graphics/gbitmap_png.c
Normal file
293
src/fw/applib/graphics/gbitmap_png.c
Normal 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;
|
||||
}
|
132
src/fw/applib/graphics/gbitmap_png.h
Normal file
132
src/fw/applib/graphics/gbitmap_png.h
Normal 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);
|
492
src/fw/applib/graphics/gbitmap_sequence.c
Normal file
492
src/fw/applib/graphics/gbitmap_sequence.c
Normal 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;
|
||||
}
|
152
src/fw/applib/graphics/gbitmap_sequence.h
Normal file
152
src/fw/applib/graphics/gbitmap_sequence.h
Normal 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);
|
54
src/fw/applib/graphics/gcolor_definitions.c
Normal file
54
src/fw/applib/graphics/gcolor_definitions.c
Normal 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)));
|
||||
}
|
331
src/fw/applib/graphics/gcolor_definitions.h
Normal file
331
src/fw/applib/graphics/gcolor_definitions.h
Normal 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
|
||||
|
243
src/fw/applib/graphics/gcontext.h
Normal file
243
src/fw/applib/graphics/gcontext.h
Normal 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
|
||||
//! Apple’s 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 can’t 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
|
265
src/fw/applib/graphics/gdraw_command.c
Normal file
265
src/fw/applib/graphics/gdraw_command.c
Normal 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;
|
||||
}
|
196
src/fw/applib/graphics/gdraw_command.h
Normal file
196
src/fw/applib/graphics/gdraw_command.h
Normal 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
|
85
src/fw/applib/graphics/gdraw_command_frame.c
Normal file
85
src/fw/applib/graphics/gdraw_command_frame.c
Normal 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;
|
||||
}
|
79
src/fw/applib/graphics/gdraw_command_frame.h
Normal file
79
src/fw/applib/graphics/gdraw_command_frame.h
Normal 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
|
142
src/fw/applib/graphics/gdraw_command_image.c
Normal file
142
src/fw/applib/graphics/gdraw_command_image.c
Normal 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;
|
||||
}
|
107
src/fw/applib/graphics/gdraw_command_image.h
Normal file
107
src/fw/applib/graphics/gdraw_command_image.h
Normal 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
|
296
src/fw/applib/graphics/gdraw_command_list.c
Normal file
296
src/fw/applib/graphics/gdraw_command_list.c
Normal 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;
|
||||
}
|
137
src/fw/applib/graphics/gdraw_command_list.h
Normal file
137
src/fw/applib/graphics/gdraw_command_list.h
Normal 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
|
94
src/fw/applib/graphics/gdraw_command_private.h
Normal file
94
src/fw/applib/graphics/gdraw_command_private.h
Normal 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[];
|
||||
};
|
216
src/fw/applib/graphics/gdraw_command_sequence.c
Normal file
216
src/fw/applib/graphics/gdraw_command_sequence.c
Normal 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;
|
||||
}
|
118
src/fw/applib/graphics/gdraw_command_sequence.h
Normal file
118
src/fw/applib/graphics/gdraw_command_sequence.h
Normal 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
|
491
src/fw/applib/graphics/gdraw_command_transforms.c
Normal file
491
src/fw/applib/graphics/gdraw_command_transforms.c
Normal 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);
|
||||
}
|
187
src/fw/applib/graphics/gdraw_command_transforms.h
Normal file
187
src/fw/applib/graphics/gdraw_command_transforms.h
Normal 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);
|
586
src/fw/applib/graphics/gpath.c
Normal file
586
src/fw/applib/graphics/gpath.c
Normal 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]);
|
||||
}
|
||||
}
|
204
src/fw/applib/graphics/gpath.h
Normal file
204
src/fw/applib/graphics/gpath.h
Normal 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
|
167
src/fw/applib/graphics/gpath_builder.c
Normal file
167
src/fw/applib/graphics/gpath_builder.c
Normal 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);
|
||||
}
|
112
src/fw/applib/graphics/gpath_builder.h
Normal file
112
src/fw/applib/graphics/gpath_builder.h
Normal 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
|
717
src/fw/applib/graphics/graphics.c
Normal file
717
src/fw/applib/graphics/graphics.c
Normal 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;
|
||||
}
|
137
src/fw/applib/graphics/graphics.h
Normal file
137
src/fw/applib/graphics/graphics.h
Normal 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
|
334
src/fw/applib/graphics/graphics_bitmap.c
Normal file
334
src/fw/applib/graphics/graphics_bitmap.c
Normal 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);
|
||||
}
|
67
src/fw/applib/graphics/graphics_bitmap.h
Normal file
67
src/fw/applib/graphics/graphics_bitmap.h
Normal 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);
|
1489
src/fw/applib/graphics/graphics_circle.c
Normal file
1489
src/fw/applib/graphics/graphics_circle.c
Normal file
File diff suppressed because it is too large
Load diff
167
src/fw/applib/graphics/graphics_circle.h
Normal file
167
src/fw/applib/graphics/graphics_circle.h
Normal 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
|
56
src/fw/applib/graphics/graphics_circle_private.h
Normal file
56
src/fw/applib/graphics/graphics_circle_private.h
Normal 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);
|
830
src/fw/applib/graphics/graphics_line.c
Normal file
830
src/fw/applib/graphics/graphics_line.c
Normal 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);
|
||||
}
|
83
src/fw/applib/graphics/graphics_line.h
Normal file
83
src/fw/applib/graphics/graphics_line.h
Normal 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);
|
129
src/fw/applib/graphics/graphics_mask.c
Normal file
129
src/fw/applib/graphics/graphics_mask.c
Normal 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
|
||||
}
|
811
src/fw/applib/graphics/graphics_private.c
Normal file
811
src/fw/applib/graphics/graphics_private.c
Normal 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;
|
||||
}
|
167
src/fw/applib/graphics/graphics_private.h
Normal file
167
src/fw/applib/graphics/graphics_private.h
Normal 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);
|
339
src/fw/applib/graphics/graphics_private_raw.c
Normal file
339
src/fw/applib/graphics/graphics_private_raw.c
Normal 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,
|
||||
};
|
26
src/fw/applib/graphics/graphics_private_raw.h
Normal file
26
src/fw/applib/graphics/graphics_private_raw.h
Normal 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);
|
349
src/fw/applib/graphics/graphics_private_raw_mask.c
Normal file
349
src/fw/applib/graphics/graphics_private_raw_mask.c
Normal 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(¤t_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
|
30
src/fw/applib/graphics/graphics_private_raw_mask.h
Normal file
30
src/fw/applib/graphics/graphics_private_raw_mask.h
Normal 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);
|
256
src/fw/applib/graphics/gtransform.c
Normal file
256
src/fw/applib/graphics/gtransform.c
Normal 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);
|
||||
}
|
263
src/fw/applib/graphics/gtransform.h
Normal file
263
src/fw/applib/graphics/gtransform.h
Normal 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
|
||||
|
776
src/fw/applib/graphics/gtypes.c
Normal file
776
src/fw/applib/graphics/gtypes.c
Normal 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;
|
||||
}
|
1423
src/fw/applib/graphics/gtypes.h
Normal file
1423
src/fw/applib/graphics/gtypes.h
Normal file
File diff suppressed because it is too large
Load diff
80
src/fw/applib/graphics/perimeter.c
Normal file
80
src/fw/applib/graphics/perimeter.c
Normal 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),
|
||||
};
|
33
src/fw/applib/graphics/perimeter.h
Normal file
33
src/fw/applib/graphics/perimeter.h
Normal 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;
|
285
src/fw/applib/graphics/text.h
Normal file
285
src/fw/applib/graphics/text.h
Normal 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
|
1320
src/fw/applib/graphics/text_layout.c
Normal file
1320
src/fw/applib/graphics/text_layout.c
Normal file
File diff suppressed because it is too large
Load diff
133
src/fw/applib/graphics/text_layout_private.h
Normal file
133
src/fw/applib/graphics/text_layout_private.h
Normal 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 })
|
||||
|
304
src/fw/applib/graphics/text_render.c
Normal file
304
src/fw/applib/graphics/text_render.c
Normal 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;
|
||||
}
|
38
src/fw/applib/graphics/text_render.h
Normal file
38
src/fw/applib/graphics/text_render.h
Normal 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);
|
653
src/fw/applib/graphics/text_resources.c
Normal file
653
src/fw/applib/graphics/text_resources.c
Normal 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 */);
|
||||
}
|
151
src/fw/applib/graphics/text_resources.h
Normal file
151
src/fw/applib/graphics/text_resources.h
Normal 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);
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue