Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add node-canvas sources and examples #32

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add node-canvas sources and examples
line-o committed Jun 26, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit ef40c9dcfabf29a1058c757a5b595101a50115e3
33 changes: 33 additions & 0 deletions node-sources/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# interact with letterbox using nodejs

A small set of modules to send UDP packets that consist of the contents of a node-canvas to the octopus server.

## setup

run `npm install`

## run examples

### render

stuff on canvas and send contents to octopus

- `node render.js [<host>]`

### scan image

load image stuff on canvas and send contents to octopus
preserves the image ratio and scans from top to bottom

- `node scan-image.js <path-to-image> [<host>]`


## convert protobuf schema

There is a shell script to convert the latest .proto file to an es6 module used with all example scripts.
Calling this will output / overwrite [packet.js](./packet.js).

```
./convert-proto.js ../protobuf/schema.proto
```

28 changes: 28 additions & 0 deletions node-sources/convert-proto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node

import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { argv } from 'node:process';
import { parseSchema } from 'pbjs';

// convert .proto to es6 module

console.log(argv)
const p = resolve(argv[2])
console.log(p)

if (!p.endsWith('.proto')) {
process.exit(1)
}

const raw = readFileSync(p)
console.log(raw.toString())
const replaced = raw.toString().replace(/( \[[^\]]+\])/g, '')
console.log(replaced)

const f = parseSchema(Buffer.from(replaced))
try {
writeFileSync('packet.js', f.toJavaScript({es6:true}))
} catch (e) {
console.error(e)
}
1,089 changes: 1,089 additions & 0 deletions node-sources/package-lock.json

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions node-sources/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "canvas-letterbox",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"canvas": "^2.11.2",
"got": "^13.0.0",
"pbjs": "^0.0.14",
"protobufjs": "^7.2.3",
"skia-canvas": "^1.0.1"
}
}
1,341 changes: 1,341 additions & 0 deletions node-sources/packet.js

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions node-sources/palettes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const blackWhite = Buffer.from([0,0,0,255,255,255])
export const rainbow = Buffer.from([
228,3,3, // Red
255,140,0, // Orange
255,237,0, // Yellow
0,128,38, // Green
0,77,255, // Blue
117,7,135 // Purple/Violet
])
102 changes: 102 additions & 0 deletions node-sources/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// USAGE
// node render.js [<hostname>]

// EXAMPLE
// node render.js localhost

import { randomInt } from "node:crypto";
import { canvas, ctx, canvasToRGBFrame, virtualScreenH, virtualScreenW } from './virtual-screen.js'
import { rgbFrame, send, demoServer, addListener } from './server.js'

let windowCenters = Array(10).fill(0).map((v,i) => 4 + (18 + 8) * i)

let currentWindow = 4
let center = windowCenters[currentWindow]

async function gen(tick) {
const x = tick % virtualScreenW
const y = tick % virtualScreenH
ctx.save()

const x0 = randomInt(center-1, center+1)
const y0 = randomInt(virtualScreenH/2-1,virtualScreenH/2+1)
const x1 = randomInt(0,virtualScreenW)
const y1 = randomInt(0,virtualScreenH)

ctx.beginPath()

let sweep = ctx.createLinearGradient(x0, y0, x1, y1)
sweep.addColorStop(0, "red")
sweep.addColorStop(0.25, "orange")
sweep.addColorStop(0.5, "yellow")
sweep.addColorStop(0.75, "green")
sweep.addColorStop(1, "blue")
ctx.strokeStyle = sweep
ctx.lineWidth = 0.5
ctx.lineCap = "square"
// console.log(x0, y0, x1, y1)
ctx.moveTo(x0, y0)
ctx.lineTo(x1, y1)
ctx.closePath()
ctx.stroke()
ctx.restore()
return await canvasToRGBFrame(canvas)
}

// function mutateRandomPixel (d) {
// const i = randomInt(0, 640)
// d[i] = randomInt(0, 8)
// }

function printUsage () {
console.log("USAGE")
console.log("node render.js [<hostname>]")
console.log("EXAMPLE")
console.log("node render.js localhost")
}

// console.log(process.argv.length)
if (process.argv.length < 3 || process.argv.length > 3) {
printUsage()
process.exit(1)
}

const server = process.argv[2] ? process.argv[2] : demoServer
console.log("sending data to", server)
console.log("stop server with ctrl+c")
await new Promise(res => setTimeout(async _ => res(true), 500)) // ~60 frames per second

let t = 0

process.on('beforeExit', e => console.log("bye bye"))
addListener(msg => {
const type = msg.input_event.type
// console.log(msg.input_event)
switch(type) {
case 'BUTTON_2':
currentWindow = Math.min(currentWindow+1, 9);
break;
case 'BUTTON_3': break;
case 'BUTTON_4': break;
case 'BUTTON_5': break;
case 'BUTTON_6': break;
case 'BUTTON_7': break;
case 'BUTTON_8': break;
case 'BUTTON_9': break;
case 'BUTTON_10':
ctx.clearRect(0, 0, virtualScreenW, virtualScreenH)
break;
default:
currentWindow = Math.max(currentWindow-1, 0);
break;
}

center = windowCenters[currentWindow]
})

while (true) {
t++;
// console.log(t)
await send(server, rgbFrame(await gen(t)))
await new Promise(res => setTimeout(async _ => res(true), 600)) // ~60 frames per second
}
49 changes: 49 additions & 0 deletions node-sources/scan-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// USAGE
// node scan-image.js <path/to/image> [<hostname>]

// EXAMPLE
// node scan-image.js test-pixels.png localhost

import { resolve } from "node:path";
import { loadImage } from 'canvas'
import { ratio, virtualScreenW, virtualScreenH, canvas, ctx, canvasToRGBFrame } from './virtual-screen.js'
import { rgbFrame, close, send, demoServer } from './server.js'

async function scan (tick) {
const offset = tick % imageH
ctx.drawImage(image, 0, offset, imageW, imageW/ratio, 0, 0, virtualScreenW, virtualScreenH)
return await canvasToRGBFrame(canvas)
}

function printUsage () {
console.log("USAGE")
console.log("node scan-image.js <path/to/image> [<hostname>]")
console.log("EXAMPLE")
console.log("node scan-image.js test-pixels.png localhost")
}

// console.log(process.argv.length)
if (process.argv.length < 3 || process.argv.length > 4) {
printUsage()
process.exit(1)
}

const path = resolve(process.argv[2])
const image = await loadImage(path)
const imageW = image.width
const imageH = image.height

const server = process.argv[3] ? process.argv[3] : demoServer
console.log("sending data to", server)
console.log(imageH)

await send(server, rgbFrame(await scan(0)))

for (let t = 1; t < imageH - virtualScreenH; t++) {
console.log(t)
await send(server, rgbFrame(await scan(t)))
await new Promise(res => setTimeout(async _ => res(true), 16)) // ~60 frames per second
}

console.log("bye bye")
close()
42 changes: 42 additions & 0 deletions node-sources/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import dgram from 'node:dgram'
import { encodePacket, decodePacket } from './packet.js';

export const client = dgram.createSocket('udp4');

let listeners = [
msg => console.log(msg)
]

export function addListener (f) {
listeners.push(f)
}

client.on('message', function (msg, info) {
const decoded = decodePacket(msg)
listeners.map(f => f(decoded))
});


export const demoServer = 'remote-octopus.fly.dev'
export const port = 2342

export function frame (data, palette) {
return encodePacket({ frame: { data, palette } })
}

export function rgbFrame (data) {
return encodePacket({ rgb_frame: { data } })
}

export async function send(server, packet) {
return new Promise((resolve, reject) => {
client.send(packet, port, server, function(err, bytes) {
if (err) reject(err)
resolve(bytes)
})
})
}

export function close () {
client.close()
}
Binary file added node-sources/test-pixels.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added node-sources/test-stripes-letterbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions node-sources/virtual-screen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createCanvas } from 'canvas'

export const pixels = 640
export const windowMargin = 18
export const virtualScreenW = 10 * 8 + 9 * windowMargin;
export const virtualScreenH = 8;
export const ratio = virtualScreenW / virtualScreenH

// const attributes = { pixelFormat: 'RGB32' }
export const canvas = createCanvas(virtualScreenW, virtualScreenH)
export const ctx = canvas.getContext('2d');
ctx.antialias = 'none'

export async function canvasToRGBFrame (canvas) {
// const imageData = ctx.getImageData(0, 0, virtualScreenW, virtualScreenH);
// console.log(imageData.data)

// debug
// const out = createWriteStream('./test.png')
// const stream = canvas.createPNGStream()
// stream.pipe(out)

const targetBuffer = Buffer.from(Array(pixels*3)) // rgb_frame
const rawBGRA = canvas.toBuffer("raw")

for (let window = 0; window < 10; window++) {
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
// map target to source pixel index
const targetBufferIndex = 3 * (window * 64 + y * 8 + x)
const sourceBufferIndex = 4 * (window * (8 + windowMargin) + y * virtualScreenW + x)

// read r g b from source - source is BGRA
const b = rawBGRA[sourceBufferIndex]
const g = rawBGRA[sourceBufferIndex+1]
const r = rawBGRA[sourceBufferIndex+2]

// console.log({sourceBufferIndex, targetBufferIndex, r,g,b})
// write r g b to target
targetBuffer[targetBufferIndex] = r
targetBuffer[targetBufferIndex+1] = g
targetBuffer[targetBufferIndex+2] = b
}
}
}

// console.log(targetBuffer)
return targetBuffer
}