diff --git a/apps/scribble/ChangeLog b/apps/scribble/ChangeLog new file mode 100644 index 000000000..af7f83942 --- /dev/null +++ b/apps/scribble/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release diff --git a/apps/scribble/README.md b/apps/scribble/README.md new file mode 100644 index 000000000..dc139bc9a --- /dev/null +++ b/apps/scribble/README.md @@ -0,0 +1,25 @@ +# App Name + +Describe the app... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/scribble/add_to_apps.json b/apps/scribble/add_to_apps.json new file mode 100644 index 000000000..054f35d55 --- /dev/null +++ b/apps/scribble/add_to_apps.json @@ -0,0 +1,14 @@ +{ "id": "scribble", + "name": "Scribble", + "shortName":"Scribble", + "version":"0.01", + "description": "A keyboard on your wrist!", + "icon": "app.png", + "tags": "keyboard, text, scribble", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"scribble.app.js","url":"app.js"}, + {"name":"scribble.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/scribble/app-icon.js b/apps/scribble/app-icon.js new file mode 100644 index 000000000..b77fc9a4f --- /dev/null +++ b/apps/scribble/app-icon.js @@ -0,0 +1 @@ +var img = require("heatshrink").decompress(atob("mEwwMB/4A2/IFE+IFE+YFE84FE44FE54SEz/jAocfDAk/54EC/1/x4FC/l/z4FDCQJGD/wFD+IYBIwYSBIwf4IwhfEIwuPIwkPIwMAj//g/P/gFCkOP/AEB/8wj5+Dn0/Aoc8n/4JAU4v/8gYFBaYWAJ4MHAoPwEgMPOgUfLogJCBYQFE+AFD8BHB/EAAAV/AoYyCB4IKBc6QA==")) \ No newline at end of file diff --git a/apps/scribble/app.png b/apps/scribble/app.png new file mode 100644 index 000000000..01a6acb72 Binary files /dev/null and b/apps/scribble/app.png differ diff --git a/apps/scribble/scribble.js b/apps/scribble/scribble.js new file mode 100644 index 000000000..e8fb9e4c2 --- /dev/null +++ b/apps/scribble/scribble.js @@ -0,0 +1,469 @@ +const black = "#000000"; +const white = "#ffffff"; +const gray1 = "#444444"; +const gray2 = "#888888"; +const gray3 = "#bbbbbb"; + +const red = "#FF0000"; +const green = "#00FF00"; +const blue = "#0000FF"; + +const transp = -1; +const abc = "abcdefghijklmnopqrstuvwxyz1234567890"; +// const abc_up = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; +const uppercase = 1; +const last_layer = false; // set to true at the last layer of the tree +let chunk_size = 6; + +const font_height = 2; +const global_font = "Dennis8"; +require("FontDennis8").add(Graphics); + +const editable_buf = "hello"; + +const left = 4; +const _screen_mid = g.getWidth() / 2; +const right = 176 - 4; + +const box_size = { + w: _screen_mid - 6, + h: 45, +}; + +const spacing = 4; +const border = 4; +const top_start = 25; + +const pos_y = [ + top_start, + top_start + (box_size.h + spacing), + top_start + (box_size.h + spacing) * 2, +]; + +// list of points to render +const points = { + "3x2": [{ x: left, y: pos_y[0] }, + { x: left, y: pos_y[1] }, + { x: left, y: pos_y[2] }, + { x: _screen_mid + 2, y: pos_y[0] }, + { x: _screen_mid + 2, y: pos_y[1] }, + { x: _screen_mid + 2, y: pos_y[2] }, + ] +}; + +g.theme = { + fg: white, + bg: black, + fg2: white, + bg2: black, + fgH: black, + bgH: red, + dark: false, +}; + +const maxX = g.getWidth(); +const maxY = g.getHeight(); +const fontSize = g.getWidth() > 200 ? 2 : 1; +const rowN = 7; +const colN = 7; +const headerH = maxY / 7; +const rowH = (maxY - headerH) / rowN; +const colW = maxX / colN; + +function getRndInteger(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +class Window { + constructor(label, bgCol) { + this.label = "win_" + this.label += (typeof label !== "undefined") ? label : "Unset"; + console.log(`Constructing Window ${this.label}, args: ${arguments}`) + + this.bgCol = bgCol; + this.layers = []; + } + + push(layer) { + layer.label=`${this.layers.length}_${layer.label}`; + this.layers.push(layer); + } + pop() { + this.layers.pop(); + } + + top_layer() { + return this.layers[this.layers.length - 1]; + } + + render() { + + if (this.bgCol !== transp) { + console.log(`${this.label}: filling bg in ${this.bgCol}`); + g.setColor(this.bgCol); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + } + + + let i = 0; + this.layers.forEach((lyr) => { + // console.log(`Rendering Layer ${i} ${lyr.label}`) + i++; + lyr.render(); + }); + } +} + +class Layer { + constructor(label) { + + this.label = "lyr_" + + this.label += (typeof label !== "undefined") ? label : "Unset"; + console.log(`Constructing Layer ${this.label}, args: ${arguments}`) + this.items = []; + // console.log(`bg is ${bg} type ${typeof bg}`) + + } + + push(button) { + this.items.push(button); + } + + setLabel(label) { + this.label = label; + } + + parseTaps(xy) { + this.items.forEach(item => { + // // print(item) + if (item.was_tapped(xy)) { + // pass parent layer to the tapped button + item.callback(this); + } + }); + } + + render() { + + this.items.forEach((item) => { + + item.render(); + }); + } +} + +class BTN_layer extends Layer { + + constructor(label, layout) { + + Layer.call(this, label) + + this.alphabet = (uppercase) ? abc.toUpperCase() : abc; + console.log(`Constructing BTN_Layer ${this.label}, layout ${this.layout}`) + + if (layout in points) { + + this.create_layout(layout); + + } + else { + throw `Invalid layout passed ->[${layout}]`; + } + + // // print(this); + + } + + render() { + + Layer.prototype.render.call(this); + } + + create_layout(layout) { + + console.log(`Creating layout ${layout}`); + + let start_p = 0; + + this.items = this.push_buttons(points[layout], this.alphabet, start_p, chunk_size) + + } + + push_buttons(points, in_string, start_p) { + + items = []; + spacer = "" // char interposed b/w the two halves of text per button + + for (let i = 0; i < points.length; i++) { + substr = `${in_string.substring( + start_p, + start_p + chunk_size / 2 + )}${spacer}${in_string.substring(start_p + chunk_size / 2, start_p + chunk_size)}`; + + btn_label = + uppercase === 1 + ? substr.toUpperCase() + : substr; + + start_p += chunk_size; + + items.push( + new Button( + i, // ID of button + points[i].x, // left + points[i].y, // top + btn_label, // text to render in the button + box_size.w, // width + box_size.h, // height + g.theme.bg, // box bg + white, // box fill + black // text col + ) + ); + } + + return items; + } + + update_labels(in_string, start_p, chk_size) { + // print(`Updating labels | in_string ${in_string} start_p ${start_p} chk_size ${chk_size}`); + in_string.replace('\n', ''); // remove newlines just in case + + spacer = "" // char interposed b/w the two halves of text per button + + for (let i = 0; i < this.items.length; i++) { + + item = this.items[i]; + substr = (chk_size < 3) + ? in_string.substring(start_p + chk_size * i, start_p + (chk_size * (i + 1))) + : `${in_string.substring( + start_p + chk_size * i, + start_p + chk_size * i + chk_size / 2 + )}${spacer}${in_string.substring(start_p + chk_size * i + chk_size / 2, start_p + chk_size * i + chk_size)}`; + // // print(`(chk_size > 3): ${(chk_size > 3)}`) + // print(`Label ${i} -> ${substr}`); + item.setLabel(substr); + } + + } + + zoom_in(id) { + let start_p = id * chunk_size; + // print(`Zooming in | start_p ${start_p}`) + if (chunk_size % this.items.length !== 0) { + throw `Chunk size [${chunk_size}] does not fit #btns [${this.items.length}]` + } + subchunk_size = chunk_size / this.items.length; + + substr = this.alphabet.substring(start_p, start_p + chunk_size); + // print(`substr ${substr}`); + // print(`subchunk_size ${subchunk_size}`); + this.update_labels(substr, 0, subchunk_size); + + } + +} + +class Button { + constructor(id, x, y, text, w, h, col, bgCol, txtCol, font) { + this.id = id; + this.label = `btn_${this.id}`; + + this.text = text; + this.x1 = x; + this.y1 = y; + this.w = w; + this.h = h; + this.col = typeof col !== "undefined" ? col : black; + this.bgCol = typeof bgCol !== "undefined" ? bgCol : gray2; + this.txtCol = typeof txtCol !== "undefined" ? txtCol : black; + // this.font = font; + + this.x2 = this.x1 + this.w; + this.y2 = this.y1 + this.h; + this.center = { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; + + + console.log(`Constructed button `) + // // print(this); + } + + render() { + // console.log( + // `Button ${this.text} -> P1: (${this.x1}, ${this.y1}) | P2: (${this.x2}, ${this.y2})` + // ); + + g.setColor(this.bgCol); + g.fillRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(this.col); + g.drawRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(this.txtCol); + + g.setFontAlign(0, 0).setFont(global_font, font_height); + g.drawString(this.text, this.center.x, this.center.y); + } + + // short tap callback func + callback(parent_layer) { + // print(`Tapped button ${this.id}`); + + // this.highlight(); // TODO set up highlighting + if (last_layer) { + l_text.items[0].text += this.text; + // print(`Updated buffer to ${l_text.items[0].text}`) + parent_layer.update_labels(parent_layer.alphabet, 0, chunk_size); + last_layer = false; + } + else { + parent_layer.zoom_in(this.id); + last_layer = true; + } + } + + was_tapped(xy) { + var x = xy.x; + var y = xy.y; + + if ((x > this.x1 && x < this.x2) && (y > this.y1 && y < this.y2)) { + return true; + } + else { + return false; + } + } + + setLabel(lbl) { + // // print(`Button ${this.id}, updating label ${this.text} with ${lbl}`); + this.text = lbl; + } + + getLabel(lbl) { + + return this.label; + } + + highlight() { + + g.setColor(g.theme.bgH); + g.fillRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(g.theme.fgH); + g.drawRect(this.x1, this.y1, this.x2, this.y2); + g.setColor(this.fg); + + g.setFontAlign(0, 0).setFont(global_font, font_height); + g.drawString(this.text, this.center.x, this.center.y); + + } + +} + +class TextBox { + + constructor(x, y, text, col) { + + // x and y are the center points + this.x = x; + this.y = y; + this.text = (typeof text !== undefined) ? text : "Default"; + this.col = (typeof col !== undefined) ? col : red; + + // console.log(`Constr TextBox ${this.text} -> Center: (${this.x}, ${this.y}) | Col ${this.col}`); + } + + render() { + // console.log(`Rendering TextBox`) + + var align_center = (0, 1); + var align_right = (0, 0); + alignment = (g.stringWidth(this.text) < g.getWidth()) ? align_center : align_right; + // coords = (g.stringWidth(this.text) < g.getWidth()- 20) ? {x:this.x, y:this.y} : {x:g.getWidth()-border, y:this.y} + coords = { x: this.x, y: this.y }; + g.setColor(this.col); + g.setFontAlign(0, 0).setFont(global_font, font_height); + g.drawString(this.text, coords.x, coords.y); + + } +} + +/* Screen refresh *************************************/ + +function draw(obj) { + console.log("draw()"); + obj.render(); +} + +let tickTimer; + +function clearTickTimer() { + if (tickTimer) { + clearTimeout(tickTimer); + tickTimer = undefined; + } +} + +function queueNextTick() { + clearTickTimer(); + tickTimer = setTimeout(tick, 5000); +} + +function tick() { + console.log("tick"); + draw(window); + // queueNextTick(); +} + +/* Init **********************************************/ + +var window = new Window("abc", red); + +var l_btns = new BTN_layer("btns", "3x2"); + +var l_text = new Layer("text"); // black + +var box = new TextBox( + _screen_mid, + 12, + editable_buf, + white +); + +l_text.push(box); + +window.push(l_text); +window.push(l_btns); + +// Set up callbacks for touches + +Bangle.on('touch', function (button, xy) { + + window.top_layer().parseTaps(xy); + window.render(); + +}); + +Bangle.on('swipe', function (direction) { + + console.log(`Swipe dir ${direction}`); + + if (direction === -1) { // left + + l_text.items[0].text = l_text.items[0].text.slice(0, -1); + + } else if (direction == 1) { // right + + l_text.items[0].text += ' '; + + } + window.render(); + +}); + +// Clear the screen once, at startup +g.clear(); + +// Start ticking +tick(); \ No newline at end of file