foxxyz / loupedeck

Node.js API for Loupedeck Controllers
MIT License
87 stars 10 forks source link

Feature Request: Support for new device "Loupedesk Live S" #19

Closed asdf23 closed 1 year ago

asdf23 commented 1 year ago

A new device is out it has 2 rotation knobs (working) --4-- 6 physical buttons, (working) and a touch screen? divided into a 3x5 (3 high 5 wide) buttons. The existing software works for the physical knobs/buttons. I could not get the events touchstart,touchend,touchmove, to work nor the .getInfo method to return. Hooking these events or calling .getInfo just sort of hangs the program.

The lsusb -v for this device is:

Bus 001 Device 008: ID 2ec2:0006 Loupedeck Loupedeck Live S
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass          239 Miscellaneous Device
  bDeviceSubClass         2 
  bDeviceProtocol         1 Interface Association
  bMaxPacketSize0        64
  idVendor           0x2ec2 
  idProduct          0x0006 
  bcdDevice            1.00
  iManufacturer           1 Loupedeck
  iProduct                2 Loupedeck Live S
  iSerial                 3 LDD2201012022390100260B1003
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x009a
    bNumInterfaces          4
    bConfigurationValue     1
    iConfiguration          4 Configuration
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              100mA
    Interface Association:
      bLength                 8
      bDescriptorType        11
      bFirstInterface         0
      bInterfaceCount         2
      bFunctionClass          2 Communications
      bFunctionSubClass       2 Abstract (modem)
      bFunctionProtocol       0 
      iFunction               5 Loupedeck
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         2 Communications
      bInterfaceSubClass      2 Abstract (modem)
      bInterfaceProtocol      1 AT-commands (v.25ter)
      iInterface              5 Loupedeck
      CDC Header:
        bcdCDC               1.10
      CDC Call Management:
        bmCapabilities       0x03
          call management
          use DataInterface
        bDataInterface          1
      CDC ACM:
        bmCapabilities       0x06
          sends break
          line coding and serial state
      CDC Union:
        bMasterInterface        0
        bSlaveInterface         1 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x82  EP 2 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               7
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        1
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass        10 CDC Data
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
      iInterface              6 CDC DATA interface
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x01  EP 1 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               1
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               1
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        2
      bAlternateSetting       0
      bNumEndpoints           0
      bInterfaceClass         1 Audio
      bInterfaceSubClass      1 Control Device
      bInterfaceProtocol      0 
      iInterface              7 Loupedeck Live
      AudioControl Interface Descriptor:
        bLength                 9
        bDescriptorType        36
        bDescriptorSubtype      1 (HEADER)
        bcdADC               1.00
        wTotalLength       0x0009
        bInCollection           1
        baInterfaceNr(0)        3
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        3
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         1 Audio
      bInterfaceSubClass      3 MIDI Streaming
      bInterfaceProtocol      0 
      iInterface              8 MIDI Streaming
      MIDIStreaming Interface Descriptor:
        bLength                 7
        bDescriptorType        36
        bDescriptorSubtype      1 (HEADER)
        bcdADC               1.00
        wTotalLength       0x001c
      MIDIStreaming Interface Descriptor:
        bLength                 6
        bDescriptorType        36
        bDescriptorSubtype      2 (MIDI_IN_JACK)
        bJackType               1 Embedded
        bJackID                 1
        iJack                   9 First jack (IN)
      MIDIStreaming Interface Descriptor:
        bLength                 6
        bDescriptorType        36
        bDescriptorSubtype      2 (MIDI_IN_JACK)
        bJackType               2 External
        bJackID                 2
        iJack                  10 Second jack (IN)
      MIDIStreaming Interface Descriptor:
        bLength                 9
        bDescriptorType        36
        bDescriptorSubtype      3 (MIDI_OUT_JACK)
        bJackType               1 Embedded
        bJackID                 3
        bNrInputPins            1
        baSourceID( 0)          2
        BaSourcePin( 0)         1
        iJack                  11 Third jack (OUT)
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x83  EP 3 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               1
        MIDIStreaming Endpoint Descriptor:
          bLength                 5
          bDescriptorType        37
          bDescriptorSubtype      1 (GENERAL)
          bNumEmbMIDIJack         1
          baAssocJackID( 0)       3
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x03  EP 3 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               1
        MIDIStreaming Endpoint Descriptor:
          bLength                 5
          bDescriptorType        37
          bDescriptorSubtype      1 (GENERAL)
          bNumEmbMIDIJack         1
          baAssocJackID( 0)       1
Device Qualifier (for other device speed):
  bLength                10
  bDescriptorType         6
  bcdUSB               2.00
  bDeviceClass          239 Miscellaneous Device
  bDeviceSubClass         2 
  bDeviceProtocol         1 Interface Association
  bMaxPacketSize0        64
  bNumConfigurations      1
Device Status:     0x0000
  (Bus Powered)
asdf23 commented 1 year ago

Strike that 6 buttons the two knobs are buttons. The output from the sample application is:

Connection successful!
Knob knobTL rotated: -1
Button pressed: knobTL
{ serial: 'LDD2201012022390100260B1003', version: '0.1.3' }
Button unpressed: knobTL
Button pressed: knobCL
{ serial: 'LDD2201012022390100260B1003', version: '0.1.3' }
Button unpressed: knobCL
Button pressed: 1
{ serial: 'LDD2201012022390100260B1003', version: '0.1.3' }
Button unpressed: 1
Button pressed: 2
{ serial: 'LDD2201012022390100260B1003', version: '0.1.3' }
Button unpressed: 2
Button pressed: 3
{ serial: 'LDD2201012022390100260B1003', version: '0.1.3' }
Button unpressed: 3
Button pressed: circle
{ serial: 'LDD2201012022390100260B1003', version: '0.1.3' }
Button unpressed: circle
Knob knobTL rotated: 1
Knob knobTL rotated: -1
Knob knobCL rotated: 1
Knob knobCL rotated: -1
const { LoupedeckDevice } = require('loupedeck')

// Detects and opens first connected device
const device = new LoupedeckDevice()

// Observe connect events
device.on('connect', () => {
    console.info('Connection successful!')
})

// React to button presses
device.on('down', ({ id }) => {
    console.info(`Button pressed: ${id}`);
    //Not working
    //try {
    //  device.getInfo().then(t=> {
    //      console.log(t);
    //  });
    //}catch(e){
    //  console.log(e);
    //}
})
device.on('up', ({ id }) => {
    console.info(`Button unpressed: ${id}`)
})

// React to knob turns
device.on('rotate', ({ id, delta }) => {
    console.info(`Knob ${id} rotated: ${delta}`)
})

//Not working
//device.on('touchstart', ({ id, delta }) => {
//  console.info(`touchstart ${delta}`)
//})
//device.on('touchmove', ({ id, delta }) => {
//  console.info(`touchmove ${delta}`)
//})
//device.on('touchend', ({ id, delta }) => {
//  console.info(`touchend ${delta}`)
//})
latenitefilms commented 1 year ago

There's a few differences for the Loupedeck Live-S in the way it handles the screens.

See:

https://github.com/CommandPost/CommandPost/blob/5c749f9468989b152db650cd4c81ad7efa00f42c/src/extensions/hs/loupedeck/init.lua#L346

asdf23 commented 1 year ago

Wow! So the change to make this work is:

constants.js 
24,27c24,26
<   center: { id: Buffer.from('\x00A'), width: 360, height: 270 }, 
<   left:   { id: Buffer.from('\x00L'), width:  60, height: 270 },  
<   right:  { id: Buffer.from('\x00R'), width:  60, height: 270 },
<   live_s: { id: Buffer.from('\x00M'), width: 480, height: 270 }
---
>   center: { id: Buffer.from('\x00A'), width: 360, height: 270 },
>   left:   { id: Buffer.from('\x00L'), width:  60, height: 270 },
>   right:  { id: Buffer.from('\x00R'), width:  60, height: 270 }
91c90

No if statements, no hard-coded values for screen size. This is very well written and future proof. The magic number constants are well documented and make since.

Especially when compared to...

--- Notes:
---  * the `response` contains the `id`, `data`, `success`.
---  * the `success` value is a boolean, `true` or `false`.
function mod.mt:updateKnobImage(knobID, imageBytes, callbackFn)
    local whichScreen = knobID > 3 and mod.screens.right or mod.screens.left
    local y = 0
    if knobID == 2 or knobID == 5 then
        y = 90
    elseif knobID == 3 or knobID == 6 then
        y = 180
    end
    return self:updateScreenImage(whichScreen, imageBytes, {x=0, y=y, w=60,h=90}, callbackFn)
end

Hard-coded, magic numbers

Really?

latenitefilms commented 1 year ago

Haha, yeah our Lua code is a mish-mash of stuff that's changed and evolved a lot over the years. I'm not a programmer by trade, so a lot of the stuff is me just trying to make stuff work. The method you mention is actually to draw an image for a specific/individual knob. I agree - it could definitely be better, but hey, it works, and has clearly pointed you in the right direction, which was my original intention.

foxxyz commented 1 year ago

@asdf23 thank you for providing this valuable info! I will review your PR and was wondering if you got getInfo() to work now?

Also, can you confirm the version of firmware you are running? I had someone else report that the new devices are running 0.2.8, but I haven't been able to see this version available for download anywhere and it would be good to know if it works with this library.

asdf23 commented 1 year ago

Functionalty seems completely working (physical buttons, knobs, touchevents, vibration?!!, writing to the screen). Prediction as to where the virtual buttons are is off. Not surprising since the device is a different shape... actually I never saw the other devices so I don't really know.

About the device's screen: The right edge screen ends before the plastic begins so it feels like the right most column has about 6 pixels width of dead space. Meanwhile the left edge hides 19 pixels under plastic, very annoying. I'm tempted to just remove the stupid plastic grid. The break down is like this:

v--- maybe not? ---v broken device?

X axis                  width
0 19        plastic         -19
19 96   column1         -77
96 109  plastic         -13
109 187 column2         -78
187 199 plastic         -12
199 275 column3         -76
275 289 plastic         -14
289 368 column4         -79
368 380 plastic         -12
380 450 column5         -70
Past 450 there is more button looking real-estate but no actual screen.

Y axis                  width
0   6   plastic         -6
6   83  row1            -77
83  97  plastic         -14
97  173 row2            -76
173 188 plastic         -15
188 263 row3            -75
263 - ?? plastic

getInfo shows:

✅ Connected to Loupedeck at /dev/ttyACM0
Connection successful!
Device serial number LDD2201012022390100260B1003, software version 0.1.3

I got from running the code:

device.on("connect", async({ address }) => {
    console.info(`✅ Connected to Loupedeck at ${address}`)
    const { serial, version } = await device.getInfo()
    console.info(`Device serial number ${serial}, software version ${version}`)

setBrightness seems to want a number between 0 and 1. I was thinking 1-10 which I got from const MAX_BRIGHTNESS = 10 so long as your using a fraction this seems to work.

To draw on the screen I used:

const { LoupedeckDevice } = require("loupedeck");
const device = new LoupedeckDevice();
const { createCanvas } = require("canvas");
...snip...
        const canvas = createCanvas(96*5, 90*3);  // <--- not exactly right
        const context = canvas.getContext("2d", {pixelFormat: "RGB16_565"});

        let colors = ["red","blue","green","yellow","purple","gray","orange","teal","cyan","lightblue","lightgreen","lightred","#34FFaa","#aaFF33","#FFaa11"];
        var i=0;
        for(let x=0; x<5; x++) {
            for(let y=0; y<3; y++) {
                context.fillRect(x*96, y*90, 96, 90); // <--- not exactly right
                context.fillStyle = colors[i++];
            }
        }
        let buff = canvas.toBuffer("raw");
        console.log(90*90*2,buff.length);
        //width * height * 2
        device.drawBuffer({
                 id: "live_s"
                ,width: 96*5  // <--- not exactly right
                ,height: 90*3  // <--- not exactly right
                ,x: 0
                ,y: 0
                ,autoRefresh: true     // <--- what is this?  window.requestAnimationFrame (fingerscrossed) .?
            }
            ,buff
        );
...snip...

I don't know anything about canvas/animation but what I read seems that a browser presents a window.requestAnimationFrame, (I was able to use setTimeout instead). I'm not sure if I can reuse the same buffer object or what autoRefresh is for, I wasn't able to get this to do anything.

On/Off connecting.... I don't get it. Connect works perfectly the first time. Then if I .close() + Ctrl+C out of the program, start the program Ctrl+C out of the program then start the program it always connects. Unplugging the device then re-plugging then starting the program always connects. I think this is just how the device works, not a reflection on this code.

Examples:

simple - drawKey doesn't seem to work. Everything else does. The display is not updated slide-puzzle - it connects but that's all nothing seems to happen.

MostHated commented 1 year ago

It was nice to see that most things were working well out of the box in the 'simple' example, though, the big seller is the nicer screens. I have been using a Xencelabs quick key for a year or two, and the Live S seems a lot more like what I was hoping the Xencelabs would have been, the screens on it are extremely basic. Unfortunately, I as well am not very familiar with Canvas, but also general web technologies, so I just have to build from the examples. From the sound of things, though, it seems like it might just take some tweaking to get things working properly?

foxxyz commented 1 year ago

@asdf23 just a note: instead of creating the canvas yourself in your example, you can call the drawCanvas method. The callback that you supply it with will automatically give you a canvas with the right width/height so you can immediately call fillRect() etc without having to do buffer or color conversions yourself.

asdf23 commented 1 year ago

Thanks for the tip. It seems my device is broken, their are some dead pixels. I'm going to try to return it, that being the case my measurements for the button locations might be off.

asdf23 commented 1 year ago

I was able to fix the dead pixels issue, I'm not sure if the original software fixed it or if I was doing something wrong.

The screen and plastic overlay isn't exactly lined up squarely, and the red/green/blue pixels are not exactly vertical, and the plastic overlay has enough height to it that the position of your head matters, and the buttons are not square, and the spacers are not uniform, and pressing a spacer bubbles right down to the screen... I mention this to say that the positioning of the buttons is a bit of a judgement call.

With all that in mind here's an algorithm that draws each button as cleanly as possible. But I'm not sure anyone wants this in their code.

            let leftOffset = 18;
            let topOffset = 6;
            let spacerWidth = 12;
            let spacerHeight = 13;
            let buttonWidth = 79;
            let buttonHeight = 77;
            let rows = 3;
            let columns = 5;
            device.drawCanvas({id: "live_s", autoRefresh: true}, (ctx,w,h)=> {
                for(let r=0; r<rows; r++) {
                    for(let c=0; c<columns; c++) {
                        ctx.fillStyle = .....
                        ctx.fillRect(leftOffset + (c * buttonWidth) + (spacerWidth * c), topOffset + (r * buttonHeight) + (spacerHeight * r), buttonWidth, buttonHeight)
                    }
                }
                colors.push(colors.shift());
            });

yuck

MostHated commented 1 year ago

I definitely am waiting this, though, I seem to be missing something. I get

/mnt/x/GitHub/foxxyz/loupedeck/device.js:108
        if (!width) width = displayInfo.width

when trying the code you shared.

Looking at a prior snippet you posted, you had

{id: "live_s", width: 96 * 5, height: 90 * 3, x: 0, y: 0, autoRefresh: true}

But that gives:

/mnt/x/GitHub/foxxyz/loupedeck/device.js:100
        await this.send(COMMANDS.FRAMEBUFF, Buffer.concat([displayInfo.id, header, buffer]))

I tried updating my project in case there was something I was missing from a framework update, but I didn't see anything. I am sure it must just be something silly I am missing.

asdf23 commented 1 year ago

Sorry I don't understand, your getting an error message? Did you edit the constants file?

MostHated commented 1 year ago

My apologies, I took the simple/index.js demo and tried to add the code you mentioned above to it, just to see if I had any success with it. So, near the bottom of the simple example, I changed the last function to include the following:

Modified Code ```javascript async function drawKeyColors(device) { const colors = ['#f66', '#f95', '#fb4', '#fd6', '#ff9', '#be9', '#9e9', '#9db', '#9cc', '#88c', '#c9c', '#d89'] for (let i = 0; i < 14; i++) { await device.drawKey(i, (ctx, w, h) => { ctx.fillStyle = colors[i] ctx.fillRect(0, 0, w, h) }) } await device.drawScreen('center', (ctx, w, h) => { ctx.fillStyle = 'white' ctx.fillRect(0, 0, w, h) }) await device.drawScreen('right', (ctx, w, h) => { ctx.fillStyle = 'white' ctx.fillRect(0, 0, w, h) }) // --| I added the code you pasted here ----- // --| -------------------------------------- let leftOffset = 18; let topOffset = 6; let spacerWidth = 12; let spacerHeight = 13; let buttonWidth = 79; let buttonHeight = 77; let rows = 3; let columns = 5; await device.drawCanvas({ id: "live_s", width: 96 * 5, height: 90 * 3, x: 0, y: 0, autoRefresh: true }, (ctx, w, h) => { for (let r = 0; r < rows; r++) { for (let c = 0; c < columns; c++) { ctx.fillStyle = ctx.fillRect(leftOffset + (c * buttonWidth) + (spacerWidth * c), topOffset + (r * buttonHeight) + (spacerHeight * r), buttonWidth, buttonHeight) } } colors.push(colors.shift()); }); } ```

After reading your reply about a constants file, I am quite sure I missed something. I didn't realize there was a constants file, so I guess I need to look for that.

asdf23 commented 1 year ago

Install node.js , create a directory, npm init , npm install loupedeck , edit constants file, create index.js , look at samples for guidance

MostHated commented 1 year ago

Yeah, I got it figured out, thanks. Now I just need to spend some time looking into Canvas and how to work with it. :+1:


Edit : One step at a time, but its getting there, lol.

photo_2022-11-16_12-26-03

asdf23 commented 1 year ago

Back to the original software....

I can't get drawKey to work, somewhere the size of the buffer changes and I'm not sure why. Seems like this happens magically when passed through a callback, I've given up on getting this to work.

IMHO, the spaces between the keys should not be considered keys, I don't know if anyone else shares that opinion. What I did to the software was to map it out.....

This functions takes a button index and converts it to an X/Y corrdinate:

    buttonIndexToKey(buttonIndex) {
        switch(this.thisDevice) {
            default:
            case "center":
                let w = 90;
                let h = 90;
                let cols = 4;
                return {
                     width: w
                    ,height: h
                    ,x: buttonIndex % cols * w
                    ,y: Math.floor(buttonIndex / cols) * h
                };
            case "live_s":
                console.log("live_s calculation");
                let leftOffset = 18;
                let topOffset = 6;
                let spacerWidth = 12;
                let spacerHeight = 13;
                let buttonWidth = 79;
                let buttonHeight = 77;
                let rows = 3;
                let columns = 5;
                return {
                     width: buttonWidth
                    ,height: buttonHeight
                    ,x: leftOffset + ((buttonIndex % columns) * buttonWidth) + ((buttonIndex % columns) * spacerWidth)
                    ,y: topOffset + (Math.floor(buttonIndex / rows) * buttonHeight) + (Math.floor(buttonIndex / rows) * spacerHeight)
                };
        }
    }

I don't mind sharing this back, I'm not sure anyone else wants it.

By "sharing this back". I don't mean that terrible code. Instead what I would check in is something like this:

const DISPLAYS = {
    center: { id: Buffer.from('\x00A'), width: 360, height: 270, leftOffset: 0, topOffset: 0, horizontalSpacer: 0, verticleSpacer: 0, buttonWidth: 90, buttonHeight: 90, rows: 4, columns: 4 },     // "A"  0x0041
    left:   { id: Buffer.from('\x00L'), width:  60, height: 270, leftOffset: 0, topOffset: 0, horizontalSpacer: 0, verticleSpacer: 0, buttonWidth: 90, buttonHeight: 90, rows: 4, columns: 4 },     // "L"  0x004C
    right:  { id: Buffer.from('\x00R'), width:  60, height: 270, leftOffset: 0, topOffset: 0, horizontalSpacer: 0, verticleSpacer: 0, buttonWidth: 90, buttonHeight: 90, rows: 4, columns: 4 },     // "R"  0x0052
    live_s: { id: Buffer.from('\x00M'), width: 480, height: 270, leftOffset: 18, topOffset: 6, horizontalSpacer: 12, verticleSpacer: 13, buttonWidth: 79, buttonHeight: 77, rows: 5, columns: 3 },  // "M"  0x004D 
}

After that change devices.js would be updated to use these values.

It's up to @foxxyz if you want to go in that direction or not. Meanwhile thanks for the contribution this got me up and running very quickly.

Julusian commented 1 year ago

IMHO, the spaces between the keys should not be considered keys, I don't know if anyone else shares that opinion. What I did to the software was to map it out.....

I kind of agree with you, but the loupedeck software appears to not. It is sending 90x90 for each button on this model, with a horizontal padding of 0x0f at the start and end of the display. But in https://github.com/bitfocus/companion I have made it send 80x80, centered within that region. I have noticed that things look a little off center sometimes, but Im not sure its a big enough deal for me to fix it in there

latenitefilms commented 1 year ago

Fun side note: It's worth noting that some CommandPost users have actually taken off the plastic in front of the screen so they have access to the full touch screen. That's the main reason I've keep the buttons the full 90x90.

MostHated commented 1 year ago

This is great, thank you.

One small question in regard to this, if you don't mind. I attempted to add some text over the button color using the following:

ctx.font = "5px serif";
ctx.fillStyle = "white";
ctx.fillText("Hello", w, h)

Just trying to get a feel for how things end up looking but nothing seems to show up. Looking at the readme, it says supports "Setting Button Color" and "Writing screen graphics". Is that literal, as in color and graphic (as in image) only, or should things like fillText also work and I am just doing it wrong? I expect it is me / the examples I looked at online but I just wanted to make sure.

foxxyz commented 1 year ago

This is great, thank you.

One small question in regard to this, if you don't mind. I attempted to add some text over the button color using the following:

ctx.font = "5px serif";
ctx.fillStyle = "white";
ctx.fillText("Hello", w, h)

Just trying to get a feel for how things end up looking but nothing seems to show up. Looking at the readme, it says supports "Setting Button Color" and "Writing screen graphics". Is that literal, as in color and graphic (as in image) only, or should things like fillText also work and I am just doing it wrong? I expect it is me / the examples I looked at online but I just wanted to make sure.

Yeah, this works for sure. You're likely not seeing anything for two reasons: 1) 5px is incredibly small, and 2) the text is left aligned at the right bottom edge of the square at (w, h) and being rendered outside of the visible area. The coordinates are relative to the key you're targeting, so you should see something if you do this:

ctx.font = "15px serif";
ctx.fillStyle = "white";
ctx.fillText("Hello", 5, 20)

(I'm using a y coordinate of 20px because the default text baseline is alphabetic so the letters are rendered above that coordinate. You can play around with setting textAlign and textBaseline to get different alignments.)

Feel free to open another issue if you encounter anything else you think is off!

MostHated commented 1 year ago

That did the trick. I figured it had to be something silly I was doing. I wasn't quite sure what the font size was going to be relative to, so I figured starting off small was a good idea, but that was certainly too small it seems, lol. It seems I had something of a "derp" moment, as for whatever reason, I was thinking that w/h was translating to a value in the range of the logged x,y when a button was pressed, even though it clearly isn't. Now that I actually see it in action, it all came together. Thanks again for your work on this. Excited to play around with it, and will be sure to report anything I may come across. :+1: