From a322a011774949d5a5971234386b8f7ee791c81d Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Mon, 26 Feb 2024 23:57:18 +0100 Subject: [PATCH] Add text support --- display-simulator/esphome-display.js | 84 +++++++++++- display-simulator/esphome-font.js | 187 +++++++++++++++++++++++++++ display-simulator/index.html | 133 +++++++++++++++++-- 3 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 display-simulator/esphome-font.js diff --git a/display-simulator/esphome-display.js b/display-simulator/esphome-display.js index b684873..0f6b197 100644 --- a/display-simulator/esphome-display.js +++ b/display-simulator/esphome-display.js @@ -255,14 +255,55 @@ class Display { } else if (y1 == y2) { // Check for special case of a top-flat triangle this.filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color); } else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle - let x_temp = Math.floor(x1 + ((y2 - y1) / (y3 - y1)) * (x3 - x1)), + let x_temp = Math.floor(x1 + Math.floor((y2 - y1) / (y3 - y1)) * (x3 - x1)), y_temp = y2; this.filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color); this.filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); } } - image(x, y, image, align = Image.TOP_LEFT, color_on = this.COLOR_ON, color_off = this.COLOR_OFF) { + print(x, y, font, arg4, arg5 = null, arg6 = null) { + let color = this.COLOR_ON; + let align = Font.TOP_LEFT; + let text; + if (arg5 === null && arg6 === null) { + text = arg4; + } else if (arg6 === null) { + if (typeof(arg4) == "number") { + align = arg4; + } else { + color = arg4; + } + text = arg5; + } else { + color = arg4; + align = arg5; + text = arg6; + } + x = Math.floor(x); + y = Math.floor(y); + let [x_start, y_start, width, height] = this.get_text_bounds(x, y, text, font, align); + font.print(x_start, y_start, this, color, text); + } + + image(x, y, image, arg4 = null, arg5 = null, arg6 = null) { + let align = Image.TOP_LEFT; + let color_on = this.COLOR_ON; + let color_off = this.COLOR_OFF + if (typeof(arg4) == "number") { + align = arg4; + if (arg5 !== null) { + color_on = arg5; + if (arg6 !== null) { + color_off = arg6; + } + } + } else if (arg4 !== null) { + color_on = arg4; + if (arg5 !== null) { + color_off = arg5; + } + } x = Math.floor(x); y = Math.floor(y); let x_align = Math.floor(align) & Image.HORIZONTAL_ALIGNMENT; @@ -294,4 +335,43 @@ class Display { image.draw(x, y, this, color_on, color_off); } + + get_text_bounds(x, y, text, font, align) { + let [width, x_offset, baseline, height] = font.measure(text); + + let x_align = Math.floor(align) & 0x18; + let y_align = Math.floor(align) & 0x07; + let x1, y1; + + switch (x_align) { + case Font.RIGHT: + x1 = x - width; + break; + case Font.CENTER_HORIZONTAL: + x1 = x - Math.floor(width / 2); + break; + case Font.LEFT: + default: + // LEFT + x1 = x; + break; + } + + switch (y_align) { + case Font.BOTTOM: + y1 = y - height; + break; + case Font.BASELINE: + y1 = y - baseline; + break; + case Font.CENTER_VERTICAL: + y1 = y - Math.floor(height / 2); + break; + case Font.TOP: + default: + y1 = y; + break; + } + return [x1, y1, width, height]; + } } diff --git a/display-simulator/esphome-font.js b/display-simulator/esphome-font.js new file mode 100644 index 0000000..c64196b --- /dev/null +++ b/display-simulator/esphome-font.js @@ -0,0 +1,187 @@ +class Glyph { + // Code in this class was taken from esphome/components/font/font.cpp and translated to JavaScript + + constructor(data) { + this.glyph_data_ = data; + } + + draw(x, y, display, color) { + let x_at = Math.floor(x) + let y_start = Math.floor(y); + let scan_x1 = this.glyph_data_.offset_x; + let scan_y1 = this.glyph_data_.offset_y; + let scan_width = this.glyph_data_.width; + let scan_height = this.glyph_data_.height; + + let data = 0; + let max_x = x_at + scan_x1 + scan_width; + let max_y = y_start + scan_y1 + scan_height; + + for (let glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) { + for (let glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) { + let pixel_data = this.glyph_data_.data[data]; + let pixel_max_x = Math.min(max_x, glyph_x + 8); + + for (let pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) { + if (pixel_data & 0x80) { + display.draw_pixel_at(pixel_x, glyph_y, color); + } + } + } + } + } + + get_char() { + return this.glyph_data_.a_char; + } + + compare_to(str) { + // 1 -> this.char_ + // 2 -> str + for (let i = 0;; i++) { + if (this.glyph_data_.a_char.charCodeAt(i) == 0) + return true; + if (str.charCodeAt(i) == 0) + return false; + if (this.glyph_data_.a_char.charCodeAt(i) > str.charCodeAt(i)) + return false; + if (this.glyph_data_.a_char.charCodeAt(i) < str.charCodeAt(i)) + return true; + } + // this should not happen + return false; + } + + match_length(str) { + for (let i = 0;; i++) { + if (this.glyph_data_.a_char.charCodeAt(i) == 0) + return i; + if (str.charCodeAt(i) != this.glyph_data_.a_char.charCodeAt(i)) + return 0; + } + // this should not happen + return 0; + } +} + +class Font { + // Code in this class was taken from esphome/components/font/font.cpp and translated to JavaScript + static TOP = 0x00; + static CENTER_VERTICAL = 0x01; + static BASELINE = 0x02; + static BOTTOM = 0x04; + static LEFT = 0x00; + static CENTER_HORIZONTAL = 0x08; + static RIGHT = 0x10; + static TOP_LEFT = Font.TOP | Font.LEFT; + static TOP_CENTER = Font.TOP | Font.CENTER_HORIZONTAL; + static TOP_RIGHT = Font.TOP | Font.RIGHT; + static CENTER_LEFT = Font.CENTER_VERTICAL | Font.LEFT; + static CENTER = Font.CENTER_VERTICAL | Font.CENTER_HORIZONTAL; + static CENTER_RIGHT = Font.CENTER_VERTICAL | Font.RIGHT; + static BASELINE_LEFT = Font.BASELINE | Font.LEFT; + static BASELINE_CENTER = Font.BASELINE | Font.CENTER_HORIZONTAL; + static BASELINE_RIGHT = Font.BASELINE | Font.RIGHT; + static BOTTOM_LEFT = Font.BOTTOM | Font.LEFT; + static BOTTOM_CENTER = Font.BOTTOM | Font.CENTER_HORIZONTAL; + static BOTTOM_RIGHT = Font.BOTTOM | Font.RIGHT; + + constructor(data, data_nr, baseline, height) { + this.glyphs_ = []; + for (let i = 0; i < data.length; i++) { + this.glyphs_.push(new Glyph(data[i])); + } + + this.baseline_ = Math.floor(baseline); + this.height_ = Math.floor(height); + } + + match_next_glyph(str) { + let lo = 0; + let hi = this.glyphs_.length - 1; + while (lo != hi) { + let mid = Math.floor((lo + hi + 1) / 2); + if (this.glyphs_[mid].compare_to(str)) { + lo = mid; + } else { + hi = mid - 1; + } + } + let match_length = this.glyphs_[lo].match_length(str); + if (match_length <= 0) + return [-1, match_length]; + return [lo, match_length]; + } + + print(x_start, y_start, display, color, text) { + let i = 0; + let x_at = x_start; + if (text[text.length - 1] != "\u0000") { + text += "\u0000" + } + while (text[i] != "\u0000") { + let [glyph_n, match_length] = this.match_next_glyph(text.substring(i)); + if (glyph_n < 0) { + // Unknown char, skip + console.log("Encountered character without representation in font: '" + text[i] + "'"); + if (this.get_glyphs().length > 0) { + let glyph_width = this.get_glyphs()[0].glyph_data_.width; + display.filled_rectangle(x_at, y_start, glyph_width, this.height_, color); + x_at += glyph_width; + } + + i++; + continue; + } + + let glyph = this.get_glyphs()[glyph_n]; + glyph.draw(x_at, y_start, display, color); + x_at += glyph.glyph_data_.width + glyph.glyph_data_.offset_x; + i += match_length; + } + } + + measure(str) { + let i = 0; + let min_x = 0; + let has_char = false; + let x = 0; + if (str[str.length - 1] != "\u0000") { + str += "\u0000" + } + while (str[i] != "\u0000") { + let [glyph_n, match_length] = this.match_next_glyph(str.substring(i)); + if (glyph_n < 0) { + // Unknown char, skip + if (this.get_glyphs().length > 0) + x += this.get_glyphs()[0].glyph_data_.width; + i++; + continue; + } + + let glyph = this.glyphs_[glyph_n]; + if (!has_char) { + min_x = glyph.glyph_data_.offset_x; + } else { + min_x = Math.min(min_x, x + glyph.glyph_data_.offset_x); + } + x += glyph.glyph_data_.width + glyph.glyph_data_.offset_x; + + i += match_length; + has_char = true; + } + return [(x - min_x), min_x, this.baseline_, this.height_]; + } + + get_baseline() { + return this.baseline_; + } + + get_height() { + return this.height_; + } + + get_glyphs() { + return this.glyphs_; + } +} diff --git a/display-simulator/index.html b/display-simulator/index.html index 5cfadea..adbba26 100644 --- a/display-simulator/index.html +++ b/display-simulator/index.html @@ -5,6 +5,7 @@
+ Display Dimensions: + + x + +
To add resoucres like fonts or images first add them to an ESPHome project and compile it. Then load the generated file located at ".esphome/build/your-project/src/main.cpp".
@@ -95,6 +102,10 @@ this.hex_regex = /0x[0-9A-Fa-f]{2}/gm; this.image_init_regex = /(?