1031 lines
37 KiB
TypeScript
1031 lines
37 KiB
TypeScript
import net from "net";
|
||
import EventEmitter from 'node:events';
|
||
import Domain from "node:domain";
|
||
import { JSDOM } from "jsdom";
|
||
import rgba from "color-rgba";
|
||
import sharp from "sharp";
|
||
import { Iconv } from "iconv";
|
||
|
||
const iconv = new Iconv("UTF-8", "NAPLPS//TRANSLIT//IGNORE");
|
||
|
||
class ResetOptions {
|
||
/**
|
||
* If true, domain parameters are reset.
|
||
*/
|
||
domain?: boolean;
|
||
/**
|
||
* 0: nothing
|
||
* 1: set color mode 0, reset to default palette and set drawing color to white
|
||
* 2: set color mode and reset to default palette. If current color mode is 0 then treat same as 3
|
||
* 3: set color mode 1, reset to default palette and set drawing color to white
|
||
*/
|
||
color?: number;
|
||
/**
|
||
* 0: nothing
|
||
* 1: clear screen to nominal black
|
||
* 2: clear screen to current drawing color
|
||
* 3: set border to nominal black
|
||
* 4: set border to current drawing color
|
||
* 5: clear screen/border to drawing color
|
||
* 6: clear screen to drawing color and set border to nominal black
|
||
* 7: clear screen/border to nominal black
|
||
*/
|
||
screen?: number;
|
||
/**
|
||
* If true, the cursor is sent home to the top left of thedisplay area and all text parameters from the text command, the C1 set and the active field are reset to their defaults.
|
||
*/
|
||
text?: boolean;
|
||
/**
|
||
* If true, all blink processes are terminated.
|
||
*/
|
||
blink?: boolean;
|
||
fields?: boolean;
|
||
texture?: boolean;
|
||
macro?: boolean;
|
||
drcs?: boolean;
|
||
}
|
||
|
||
interface Value {
|
||
encode(width: number): Uint8Array;
|
||
}
|
||
|
||
export class SingleValue implements Value {
|
||
value: number;
|
||
constructor(value: number) {
|
||
this.value = value;
|
||
}
|
||
encode(width: number) {
|
||
var data = new Uint8Array(Array(width).fill(64));
|
||
for(var i = 0; i < width; i++)
|
||
{
|
||
data[i] |= (this.value >> ((width*6-6)-i*6)) & 63
|
||
}
|
||
return data;
|
||
}
|
||
}
|
||
|
||
export class Point implements Value {
|
||
x: number;
|
||
y: number;
|
||
z?: number;
|
||
constructor(x: number, y: number, z: number | undefined = undefined) {
|
||
this.x = x;
|
||
this.y = y;
|
||
this.z = z;
|
||
}
|
||
encode(width: number, force3d: boolean = false) {
|
||
var threeD = this.z !== undefined || force3d;
|
||
var thisz = this.z || 0;
|
||
var data = new Uint8Array(Array(width).fill(64));
|
||
var bits = width*(threeD?2:3)-1;
|
||
|
||
var xx = this.x < 0 ? -this.x-1 : this.x;
|
||
var yy = this.y < 0 ? -this.y-1 : this.y;
|
||
var zz = thisz < 0 ? -thisz-1 : thisz;
|
||
|
||
var x = Math.abs(xx*(2**bits-1));
|
||
var y = Math.abs(yy*(2**bits-1));
|
||
var z = Math.abs(zz*(2**bits-1));
|
||
|
||
if(threeD)
|
||
{
|
||
|
||
data[0] |= this.x < 0 ? 32 : 0;
|
||
data[0] |= this.y < 0 ? 8 : 0;
|
||
data[0] |= thisz < 0 ? 2 : 0;
|
||
|
||
for(var i = 0; i < width; i++)
|
||
{
|
||
data[i] |= x >> ((width*2-2)-i*2) << 4 & 48;
|
||
data[i] |= y >> ((width*2-2)-i*2) << 2 & 12;
|
||
data[i] |= z >> ((width*2-2)-i*2) & 3;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
data[0] |= this.x < 0 ? 32 : 0;
|
||
data[0] |= this.y < 0 ? 4 : 0;
|
||
|
||
for(var i = 0; i < width; i++)
|
||
{
|
||
data[i] |= x >> ((width*3-3)-i*3) << 3 & 56;
|
||
data[i] |= y >> ((width*3-3)-i*3) & 7;
|
||
}
|
||
}
|
||
|
||
return data;
|
||
}
|
||
}
|
||
|
||
export class Color implements Value {
|
||
r: number;
|
||
g: number;
|
||
b: number;
|
||
constructor(r: number, g: number, b: number) {
|
||
this.r = r;
|
||
this.g = g;
|
||
this.b = b;
|
||
}
|
||
encode(width: number) {
|
||
var data = new Uint8Array(Array(width).fill(64));
|
||
var bits = width*2;
|
||
var r = Math.abs(this.r*(2**bits-1));
|
||
var g = Math.abs(this.g*(2**bits-1));
|
||
var b = Math.abs(this.b*(2**bits-1));
|
||
|
||
for(var i = 0; i < width; i++)
|
||
{
|
||
// x1GRBGRB
|
||
data[i] |= [0,1,8,9][b >> ((width*2-2)-i*2) & 3];
|
||
data[i] |= [0,2,16,18][r >> ((width*2-2)-i*2) & 3];
|
||
data[i] |= [0,4,32,36][g >> ((width*2-2)-i*2) & 3];
|
||
}
|
||
|
||
return data;
|
||
}
|
||
}
|
||
|
||
class Options {
|
||
/**
|
||
* Dumps a bunch of useless info in the console.
|
||
*/
|
||
debug!: boolean;
|
||
|
||
pdiGr: boolean;
|
||
|
||
/**
|
||
* Enables use of the third dimension.
|
||
*/
|
||
thirdDimension!: boolean;
|
||
/**
|
||
* Length of a multi-value operand in bytes (1-8)
|
||
*/
|
||
multiLength!: number; // [1,8]
|
||
/**
|
||
* Length of a single-value operand in bytes (1-4)
|
||
*/
|
||
singleLength!: number; // [1,4]
|
||
/**
|
||
* Length of a color in the incremental point command in bytes (1-8)
|
||
*/
|
||
incrementalLength: number; // [1-8]
|
||
/**
|
||
* Sets width and height of the logical pel
|
||
*/
|
||
pelSize!: Point;
|
||
|
||
/**
|
||
* How far to move the cursor after displaying a character or a space
|
||
* 0: x1 (no gap)
|
||
* 1: x1.25 (quarter-character gap)
|
||
* 2: x1.5 (half-character gap)
|
||
* 3: proportional spacing
|
||
*/
|
||
textSpacing!: number; // [0,3]
|
||
/**
|
||
* After a character is displayed, the cursor moves in this direction
|
||
* 0: right
|
||
* 1: left
|
||
* 2: up
|
||
* 3: down
|
||
*/
|
||
textDirection!: number; // [0,3]
|
||
/**
|
||
* Characters are rotated
|
||
* 0: 0 degrees
|
||
* 1: 90 degrees
|
||
* 2: 180 degrees
|
||
* 3: 270 degrees
|
||
*/
|
||
textRotation!: number; // [0,3]
|
||
/**
|
||
* Cursor style
|
||
* 0: underscore
|
||
* 1: block
|
||
* 2: crosshair
|
||
* 3: custom
|
||
*/
|
||
cursorStyle!: number; // [0,3]
|
||
/**
|
||
* How the graphical drawing point is related to the text cursor
|
||
* 0: move together
|
||
* 1: cursor leads
|
||
* 2: drawing point leads
|
||
* 3: move independently
|
||
*/
|
||
drawingPoint!: number; // [0,3]
|
||
/**
|
||
* Row spacing
|
||
* 0: 1
|
||
* 1: 1.25
|
||
* 2: 1.5
|
||
* 3: 2
|
||
*/
|
||
rowSpacing!: number; // [0,3]
|
||
/**
|
||
* Sets the size of a character
|
||
*/
|
||
characterSize!: Point;
|
||
|
||
/**
|
||
* Fill pattern
|
||
* 0: solid
|
||
* 1: vertical hatching
|
||
* 2: horizontal hatching
|
||
* 3: vertical and horizontal cross-hatching
|
||
* 4: programmable mask A
|
||
* 5: programmable mask B
|
||
* 6: programmable mask C
|
||
* 7: programmable mask D
|
||
*/
|
||
fillPattern!: number; // [0,7]
|
||
/**
|
||
* If true, draw outlines of filled objects using current pel size
|
||
*/
|
||
drawOutline!: boolean;
|
||
/**
|
||
* Outline style
|
||
* 0: solid
|
||
* 1: dotted
|
||
* 2: dashed
|
||
* 3: dot-dash
|
||
*/
|
||
lineTexture!: number; // [0,3]
|
||
/**
|
||
* Mask size used in pattern fills for the programmable masks
|
||
*/
|
||
maskSize!: Point;
|
||
}
|
||
|
||
const DefaultOptions: Options = {
|
||
debug: true,
|
||
pdiGr: false,
|
||
|
||
thirdDimension: false,
|
||
multiLength: 3,
|
||
singleLength: 1,
|
||
incrementalLength: 3,
|
||
pelSize: new Point(0,0),
|
||
|
||
textSpacing: 0,
|
||
textDirection: 0,
|
||
textRotation: 0,
|
||
cursorStyle: 0,
|
||
drawingPoint: 0,
|
||
rowSpacing: 0,
|
||
characterSize: new Point(1/40, 5/128),
|
||
|
||
fillPattern: 0,
|
||
drawOutline: false,
|
||
lineTexture: 0,
|
||
maskSize: new Point(1/40, 5/128)
|
||
}
|
||
|
||
class DrawImageOptions
|
||
{
|
||
background?: string | object;
|
||
resize?: [number, number] | boolean;
|
||
mirrory?: boolean;
|
||
}
|
||
|
||
export class Naplps extends EventEmitter {
|
||
socket: net.Socket;
|
||
encoder: any;
|
||
|
||
options: Options = DefaultOptions;
|
||
|
||
constructor(socket: net.Socket, options = {}) {
|
||
super();
|
||
this.socket = socket;
|
||
this.options = {...this.options, ...options};
|
||
|
||
if(this.options.debug)console.log("> <%s:%d connected>", socket.remoteAddress, socket.remotePort);
|
||
|
||
socket.on("end", ()=>{
|
||
if(this.options.debug)console.log("> <%s:%d disconnected>", socket.remoteAddress, socket.remotePort);
|
||
this.emit("end");
|
||
});
|
||
socket.on("close", hadError=>this.emit("close", hadError));
|
||
socket.on("error", err=>this.emit("error", err));
|
||
|
||
socket.on("data", data=>{
|
||
if(this.options.debug)console.log("<", data);
|
||
for(var byte of data)
|
||
this.emit("data", byte);
|
||
});
|
||
}
|
||
|
||
write(data: string | Uint8Array<ArrayBufferLike>) {
|
||
if(this.options.debug)console.log(">", data);
|
||
return this.socket.write(data, "latin1");
|
||
}
|
||
|
||
writeBytes(...bytes: number[]) {
|
||
return this.write(new Uint8Array(bytes));
|
||
}
|
||
|
||
text(data: string) {
|
||
if(!this.options.pdiGr) this.textMode();
|
||
this.supplementaryModeGr();
|
||
this.write(iconv.convert(data));
|
||
if(!this.options.pdiGr) this.pdiMode();
|
||
this.pdiModeGr();
|
||
}
|
||
|
||
/**
|
||
* Switches terminal to graphics mode
|
||
*/
|
||
init() {
|
||
//return this.writeBytes(0x1b, 0x31);
|
||
return this.writeBytes(0x1b, 0x25, 0x41);
|
||
}
|
||
|
||
/**
|
||
* Switches terminal back to text mode
|
||
*/
|
||
uninit() {
|
||
//return this.writeBytes(0x1b, 0x32);
|
||
return this.writeBytes(0x1b, 0x25, 0x40);
|
||
}
|
||
|
||
/**
|
||
* Shifts to text mode
|
||
*/
|
||
textMode() {
|
||
return this.writeBytes(0x0f);
|
||
}
|
||
|
||
/**
|
||
* Shifts to command mode
|
||
*/
|
||
pdiMode() {
|
||
return this.writeBytes(0x0e);
|
||
}
|
||
|
||
supplementaryMode() {
|
||
return this.writeBytes(0x1b, 0x6e);
|
||
}
|
||
|
||
mosaicsMode() {
|
||
return this.writeBytes(0x1b, 0x6f);
|
||
}
|
||
|
||
pdiModeGr() {
|
||
return this.writeBytes(0x1b, 0x7e);
|
||
}
|
||
|
||
supplementaryModeGr() {
|
||
return this.writeBytes(0x1b, 0x7d);
|
||
}
|
||
|
||
mosaicsModeGr() {
|
||
return this.writeBytes(0x1b, 0x7c);
|
||
}
|
||
|
||
/**
|
||
* Non-selective reset: resets most of the settings to their default state
|
||
* @param row Reposition cursor to given row
|
||
* @param column Reposition cursor to given column
|
||
*/
|
||
nsr(row: number | undefined = undefined, column: number | undefined = undefined) {
|
||
var r = row || 0 & 63 | 64;
|
||
var c = column || 0 & 63 | 64;
|
||
return this.writeBytes(0x1f, r, c);
|
||
}
|
||
|
||
/**
|
||
* Immediately terminate the processing of currently executing macros
|
||
*/
|
||
cancel() {
|
||
return this.writeBytes(0x18);
|
||
}
|
||
|
||
/**
|
||
* beep
|
||
*/
|
||
bell() {
|
||
return this.writeBytes(0x07);
|
||
}
|
||
|
||
defineMacro(number: number) {
|
||
return this.writeBytes(0x80, number);
|
||
}
|
||
|
||
defineExecMacro(number: number) {
|
||
return this.writeBytes(0x81, number);
|
||
}
|
||
|
||
defineTransmitMacro(number: number) {
|
||
return this.writeBytes(0x82, number);
|
||
}
|
||
|
||
defineDrcs(number: number) {
|
||
return this.writeBytes(0x83, number);
|
||
}
|
||
|
||
defineTexture(number: number) {
|
||
return this.writeBytes(0x84, number-0x40);
|
||
}
|
||
|
||
defineEnd() {
|
||
return this.writeBytes(0x85);
|
||
}
|
||
|
||
protect(protect: boolean) {
|
||
return this.writeBytes(protect?0x90:0x9f);
|
||
}
|
||
|
||
repeat(times: number) {
|
||
this.writeBytes(0x86);
|
||
this.write(new SingleValue(times).encode(1));
|
||
}
|
||
|
||
repeatToEol(times: number) {
|
||
return this.writeBytes(0x87);
|
||
}
|
||
|
||
reverseVideo(reverse: boolean) {
|
||
return this.writeBytes(reverse?0x88:0x89);
|
||
}
|
||
|
||
smallText() {
|
||
return this.writeBytes(0x8a);
|
||
}
|
||
|
||
mediumText() {
|
||
return this.writeBytes(0x8b);
|
||
}
|
||
|
||
normalText() {
|
||
return this.writeBytes(0x8c);
|
||
}
|
||
|
||
doubleHeightText() {
|
||
return this.writeBytes(0x8d);
|
||
}
|
||
|
||
doubleSizeText() {
|
||
return this.writeBytes(0x8f);
|
||
}
|
||
|
||
wordWrap(wrap: boolean) {
|
||
return this.writeBytes(wrap?0x95:0x96);
|
||
}
|
||
|
||
scroll(scroll: boolean) {
|
||
return this.writeBytes(scroll?0x97:0x98);
|
||
}
|
||
|
||
underline(underline: boolean) {
|
||
return this.writeBytes(underline?0x99:0x9a);
|
||
}
|
||
|
||
flashCursor() {
|
||
return this.writeBytes(0x9b);
|
||
}
|
||
|
||
steadyCursor() {
|
||
return this.writeBytes(0x9c);
|
||
}
|
||
|
||
cursorOff() {
|
||
return this.writeBytes(0x9d);
|
||
}
|
||
|
||
simpleBlink(blink: boolean) {
|
||
return this.writeBytes(blink?0x8e:0x9e);
|
||
}
|
||
|
||
/**
|
||
* Selectively reset part of graphics to their default values
|
||
* @param options Reset options
|
||
*/
|
||
reset(options: ResetOptions = {}) {
|
||
var data = new Uint8Array([0x20 | (this.options.pdiGr?0x80:0), 64, 64]);
|
||
data[1] |= options.domain ? 1 : 0;
|
||
data[1] |= options.color||0 << 1;
|
||
data[1] |= options.screen||0 << 3;
|
||
data[2] |= options.text ? 1 : 0;
|
||
data[2] |= options.blink ? 2 : 0;
|
||
data[2] |= options.fields ? 4 : 0;
|
||
data[2] |= options.texture ? 8 : 0;
|
||
data[2] |= options.macro ? 16 : 0;
|
||
data[2] |= options.drcs ? 32 : 0;
|
||
return this.write(data);
|
||
}
|
||
|
||
setDomain() {
|
||
var data = new Uint8Array([0x21 | (this.options.pdiGr?0x80:0), 64]);
|
||
data[1] |= this.options.singleLength-1;
|
||
data[1] |= (this.options.multiLength-1) << 2;
|
||
data[1] |= this.options.thirdDimension ? 32 : 0;
|
||
this.write(data);
|
||
this.write(this.options.pelSize.encode(this.options.multiLength));
|
||
}
|
||
|
||
setText() {
|
||
var data = new Uint8Array([0x22 | (this.options.pdiGr?0x80:0), 64, 64]);
|
||
data[1] |= this.options.textSpacing << 4;
|
||
data[1] |= this.options.textDirection << 2;
|
||
data[1] |= this.options.textRotation;
|
||
data[2] |= this.options.cursorStyle << 4;
|
||
data[2] |= this.options.drawingPoint << 2;
|
||
data[2] |= this.options.rowSpacing;
|
||
this.write(data);
|
||
this.write(this.options.characterSize.encode(this.options.multiLength));
|
||
}
|
||
|
||
setTexture() {
|
||
var data = new Uint8Array([0x23 | (this.options.pdiGr?0x80:0), 64]);
|
||
data[1] |= this.options.fillPattern << 3;
|
||
data[1] |= this.options.drawOutline ? 4 : 0;
|
||
data[1] |= this.options.lineTexture;
|
||
this.write(data);
|
||
this.write(this.options.maskSize.encode(this.options.multiLength));
|
||
}
|
||
|
||
pointSetAbs(point: Point) {
|
||
this.writeBytes(0x24 | (this.options.pdiGr?0x80:0));
|
||
this.write(point.encode(this.options.multiLength));
|
||
}
|
||
pointSetRel(point: Point) {
|
||
this.writeBytes(0x25 | (this.options.pdiGr?0x80:0));
|
||
this.write(point.encode(this.options.multiLength));
|
||
}
|
||
pointAbs(points: Point[]) {
|
||
this.writeBytes(0x26 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
pointRel(points: Point[]) {
|
||
this.writeBytes(0x27 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
lineAbs(points: Point[]) {
|
||
this.writeBytes(0x28 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
lineRel(points: Point[]) {
|
||
this.writeBytes(0x29 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setLineAbs(points: Point[]) {
|
||
this.writeBytes(0x2a | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setLineRel(points: Point[]) {
|
||
this.writeBytes(0x2b | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
arcOutlined(points: Point[]) {
|
||
this.writeBytes(0x2c | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
arcFilled(points: Point[]) {
|
||
this.writeBytes(0x2d | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setArcOutlined(points: Point[]) {
|
||
this.writeBytes(0x2e | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setArcFilled(points: Point[]) {
|
||
this.writeBytes(0x2f | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
rectOutlined(points: Point[]) {
|
||
this.writeBytes(0x30 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
rectFilled(points: Point[]) {
|
||
this.writeBytes(0x31 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setRectOutlined(points: Point[]) {
|
||
this.writeBytes(0x32 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setRectFilled(points: Point[]) {
|
||
this.writeBytes(0x33 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
polyOutlined(points: Point[]) {
|
||
this.writeBytes(0x34 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
polyFilled(points: Point[]) {
|
||
this.writeBytes(0x35 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setPolyOutlined(points: Point[]) {
|
||
this.writeBytes(0x36 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setPolyFilled(points: Point[]) {
|
||
this.writeBytes(0x37 | (this.options.pdiGr?0x80:0));
|
||
points.forEach(p=>this.write(p.encode(this.options.multiLength)));
|
||
}
|
||
setField(size: Point | undefined = undefined, origin: Point | undefined = undefined) {
|
||
this.writeBytes(0x38 | (this.options.pdiGr?0x80:0));
|
||
if(origin !== undefined) this.write(origin.encode(this.options.multiLength));
|
||
if(size !== undefined) this.write(size.encode(this.options.multiLength));
|
||
}
|
||
incrementalPoint(data: Color[]) {
|
||
this.writeBytes(0x39 | (this.options.pdiGr?0x80:0));
|
||
this.write(new SingleValue(this.options.incrementalLength*6).encode(1));
|
||
data.forEach(d=>this.write(d.encode(this.options.incrementalLength)));
|
||
}
|
||
// TODO: 0x3a, 0x3b
|
||
setColor(color: Color) {
|
||
this.writeBytes(0x3c | (this.options.pdiGr?0x80:0));
|
||
this.write(color.encode(this.options.multiLength));
|
||
}
|
||
wait(timeout: number){
|
||
this.writeBytes(0x3d | (this.options.pdiGr?0x80:0), 0x5c);
|
||
var secs = Math.floor(timeout*10) & 63 | 64;
|
||
// TODO: timeout >= 6.4s
|
||
this.writeBytes(secs);
|
||
}
|
||
selectColor(fg: SingleValue | undefined = undefined, bg: SingleValue | undefined = undefined) {
|
||
this.writeBytes(0x3e | (this.options.pdiGr?0x80:0));
|
||
if(fg !== undefined) this.write(fg.encode(this.options.singleLength));
|
||
if(bg !== undefined) this.write(bg.encode(this.options.singleLength));
|
||
}
|
||
blink(blinkto: SingleValue, on: number, off: number, delay: number) {
|
||
this.writeBytes(0x3f | (this.options.pdiGr?0x80:0));
|
||
this.write(blinkto.encode(this.options.singleLength));
|
||
this.writeBytes(
|
||
Math.floor(on*10) & 63 | 64,
|
||
Math.floor(off*10) & 63 | 64,
|
||
Math.floor(delay*10) & 63 | 64,
|
||
);
|
||
}
|
||
|
||
async drawImage(file: string, size = new Point(1,0.75), origin = new Point(0,0), options: DrawImageOptions = {}) {
|
||
var pelSize = this.options.pelSize;
|
||
// TODO: paletted images
|
||
var image = sharp(file);
|
||
if(options.resize) image = image.resize(options.resize[0], options.resize[1])
|
||
var { data, info } = await image.flatten({ background: options.background }).raw().toBuffer({ resolveWithObject: true });
|
||
this.options.pelSize = new Point(size.x/(info.width), size.y/(info.height+1));
|
||
this.setDomain();
|
||
var s = new Point(size.x, -size.y)
|
||
var o = new Point(origin.x, size.y+origin.y/*-size.y/info.height*/);
|
||
//TODO: options.mirrory
|
||
this.setField(size,origin);
|
||
var points: Color[] = [];
|
||
for(var i = 0; i < info.size; i += 3)
|
||
{
|
||
var pt = new Color(data[i]/255, data[i+1]/255, data[i+2]/255);
|
||
points.push(pt);
|
||
}
|
||
this.selectColor();
|
||
this.pointSetAbs(origin);
|
||
this.incrementalPoint(points);
|
||
//console.log(this.options.pelSize, points.length);
|
||
this.options.pelSize = pelSize;
|
||
this.setDomain();
|
||
}
|
||
|
||
async drawSvg(file: string, size = new Point(1,0.75), origin = new Point(0,0)) {
|
||
var win = (await JSDOM.fromFile(file, { contentType: "text/svg+xml" })).window
|
||
var node = win.document;
|
||
var viewbox = [0,0,100,100];
|
||
|
||
var conv = (x: number, y: number, add: boolean = true) => new Point(
|
||
(x+viewbox[0])/viewbox[2]*size.x + (add?origin.x:0),
|
||
(add?size.y:0) - (y+viewbox[1])/viewbox[3]*size.y - (add?origin.y:0)
|
||
);
|
||
|
||
var rgbColor = (color: string) => {
|
||
var c = rgba(color);
|
||
if(c.length < 4 || c[3] == 0) return false;
|
||
return new Color((c[0]||0)/255, (c[1]||0)/255, (c[2]||0)/255);
|
||
};
|
||
|
||
var drawChildren = async (children: HTMLCollection)=>{
|
||
for(const n of children)
|
||
{
|
||
//console.log(n.nodeName);
|
||
var style = win.getComputedStyle(n);
|
||
var fill = rgbColor(style.fill || n.getAttribute("fill") || "");
|
||
var stroke = rgbColor(style.stroke || n.getAttribute("stroke") || "");
|
||
switch(n.nodeName){
|
||
case "svg": {
|
||
var vbox = n.getAttribute("viewBox") || "0 0 100 100"
|
||
viewbox = vbox.split(" ").map(v=>parseFloat(v));
|
||
//console.log("viewbox",viewbox);
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
this.setRectFilled([origin,size]);
|
||
}
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
this.setRectOutlined([origin,size]);
|
||
}
|
||
} break;
|
||
case "line": {
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
var x1 = parseFloat(n.getAttribute("x1")||"0");
|
||
var y1 = parseFloat(n.getAttribute("y1")||"0");
|
||
var x2 = parseFloat(n.getAttribute("x2")||"0");
|
||
var y2 = parseFloat(n.getAttribute("y2")||"0");
|
||
this.setColor(stroke || new Color(0,0,0));
|
||
//console.log(conv(x1,y1),conv(x2, y2));
|
||
this.setLineAbs([conv(x1,y1),conv(x2, y2)]);
|
||
} break;
|
||
case "rect": {
|
||
var x = parseFloat(n.getAttribute("x")||"0");
|
||
var y = parseFloat(n.getAttribute("y")||"0");
|
||
var width = parseFloat(n.getAttribute("width")||"0");
|
||
var height = parseFloat(n.getAttribute("height")||"0");
|
||
//console.log(conv(x,y),conv(width, height, false));
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
this.setRectFilled([conv(x,y),conv(width, height, false)]);
|
||
}
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
this.setRectOutlined([conv(x,y),conv(width, height, false)]);
|
||
}
|
||
} break;
|
||
case "circle": {
|
||
var cx = parseFloat(n.getAttribute("cx")||"0");
|
||
var cy = parseFloat(n.getAttribute("cy")||"0");
|
||
var r = parseFloat(n.getAttribute("r")||"0");
|
||
var pts: Point[] = [];
|
||
pts.push(conv(cx,cy-r));
|
||
pts.push(conv(0,2*r, false));
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
//this.setRectFilled([conv(x,y),conv(width, height, false)]);
|
||
this.setArcFilled(pts);
|
||
}
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
//this.setRectOutlined([conv(x,y),conv(width, height, false)]);
|
||
this.setArcOutlined(pts);
|
||
}
|
||
} break;
|
||
case "ellipse": {
|
||
var cx = parseFloat(n.getAttribute("cx")||"0");
|
||
var cy = parseFloat(n.getAttribute("cy")||"0");
|
||
var rx = parseFloat(n.getAttribute("rx")||"0");
|
||
var ry = parseFloat(n.getAttribute("ry")||"0");
|
||
var pts: Point[] = [];
|
||
pts.push(conv(cx,cy-ry));
|
||
pts.push(conv(rx,ry, false));
|
||
pts.push(conv(-rx,ry, false));
|
||
pts.push(conv(-rx,-ry, false));
|
||
pts.push(conv(rx,-ry, false));
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
//this.setRectFilled([conv(x,y),conv(width, height, false)]);
|
||
this.setArcFilled(pts);
|
||
}
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
//this.setRectOutlined([conv(x,y),conv(width, height, false)]);
|
||
this.setArcOutlined(pts);
|
||
}
|
||
} break;
|
||
case "polyline":
|
||
case "polygon": {
|
||
var points = n.getAttribute("points")?.split(" ")||[];
|
||
if(points.length > 0)
|
||
{
|
||
/*var pts = points.map(v=>{
|
||
var pt = v.split(",").map(v=>parseFloat(v));
|
||
var co = conv(pt[0], pt[1]);
|
||
console.log(pt, co)
|
||
return co;
|
||
});*/
|
||
var pts: Point[] = [];
|
||
var polypts: Point[] = [];
|
||
var parsed = points.map(v=>parseFloat(v));
|
||
//console.log(conv(parsed[0], parsed[1]));
|
||
polypts.push(conv(parsed[0], parsed[1]));
|
||
for(var i = 2; i < parsed.length; i+=2)
|
||
{
|
||
var u = conv(parsed[i], parsed[i+1]);
|
||
var v = conv(parsed[i-2], parsed[i-1]);
|
||
pts.push(v);
|
||
pts.push(new Point(u.x-v.x, u.y-v.y));
|
||
polypts.push(new Point(u.x-v.x, u.y-v.y));
|
||
}
|
||
//console.log(pts);
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
this.setPolyFilled(polypts);
|
||
}
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
if(n.nodeName == "polyline")
|
||
this.setLineRel(pts);
|
||
else
|
||
this.setPolyOutlined(polypts);
|
||
}
|
||
}
|
||
} break;
|
||
case "path": {
|
||
// TODO: use arcs
|
||
//this.setColor(rgbColor(style.fill));
|
||
var points = n.getAttribute("d")?.split(" ")||[];
|
||
var mode = 'M';
|
||
var why = false;
|
||
var pts: Point[] = [];
|
||
var ptss: [number,number][] = [];
|
||
for(var pt of points)
|
||
{
|
||
var val = parseFloat(pt);
|
||
if(Number.isNaN(val))
|
||
{
|
||
mode = pt;
|
||
}
|
||
else
|
||
{
|
||
if(why)
|
||
{
|
||
ptss[ptss.length-1][1] = val;
|
||
}
|
||
else
|
||
{
|
||
ptss.push([val,0]);
|
||
}
|
||
why = !why;
|
||
}
|
||
}
|
||
for(var i = 0; i < ptss.length; i++)
|
||
{
|
||
if(i==0) pts.push(conv(ptss[i][0], ptss[i][1]));
|
||
else pts.push(conv(ptss[i][0]-ptss[i-1][0], ptss[i][1]-ptss[i-1][1], false));
|
||
}
|
||
//console.log(pts);
|
||
//if(pts.length <= 2){
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
this.setPolyFilled(pts);
|
||
}
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
this.setPolyOutlined(pts);
|
||
}
|
||
/*}
|
||
else {
|
||
this.setColor(rgbColor(style.fill));
|
||
this.setArcFilled(pts);
|
||
}*/
|
||
} break;
|
||
case "text": {
|
||
if(n.textContent)
|
||
{
|
||
var x = parseFloat(n.getAttribute("x")||"0");
|
||
var y = parseFloat(n.getAttribute("y")||"0");
|
||
var width = parseFloat(n.getAttribute("width")||viewbox[2].toString());
|
||
var height = parseFloat(n.getAttribute("height")||viewbox[3].toString());
|
||
var fontstyle = style.fontSize || n.getAttribute("font-size") || "normal";
|
||
var wordwrap = n.getAttribute("word-wrap") != null;
|
||
switch(fontstyle) {
|
||
case "small":
|
||
this.smallText();
|
||
break;
|
||
case "medium":
|
||
this.mediumText();
|
||
break;
|
||
case "double":
|
||
this.doubleSizeText();
|
||
break;
|
||
case "doubleheight":
|
||
this.doubleHeightText();
|
||
break;
|
||
case "normal":
|
||
default:
|
||
this.normalText();
|
||
break;
|
||
}
|
||
if(n.getAttribute("width") || n.getAttribute("height"))
|
||
this.setField(conv(x,y),conv(width,height,false));
|
||
this.wordWrap(wordwrap);
|
||
this.pointSetAbs(conv(x,y));
|
||
this.setColor(fill || new Color(0,0,0));
|
||
this.text(n.textContent.replaceAll("\n","\r\n"));
|
||
this.wordWrap(false);
|
||
switch(fontstyle) {
|
||
case "small":
|
||
case "medium":
|
||
case "double":
|
||
case "doubleheight":
|
||
this.normalText();
|
||
break;
|
||
}
|
||
}
|
||
} break;
|
||
case "image": {
|
||
var x = parseFloat(n.getAttribute("x")||"0");
|
||
var y = parseFloat(n.getAttribute("y")||"0");
|
||
var width = parseFloat(n.getAttribute("width")||"0");
|
||
var height = parseFloat(n.getAttribute("height")||"0");
|
||
var href = n.getAttribute("href");
|
||
if(href)
|
||
await this.drawImage(href, conv(width,height,false), conv(x,y), { background: style.fill || n.getAttribute("fill") || undefined });
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
this.setRectOutlined([conv(x,y),conv(width,height,false)]);
|
||
}
|
||
} break;
|
||
case "use": {
|
||
var x = parseFloat(n.getAttribute("x")||"0");
|
||
var y = parseFloat(n.getAttribute("y")||"0");
|
||
var width = parseFloat(n.getAttribute("width")||"0");
|
||
var height = parseFloat(n.getAttribute("height")||"0");
|
||
var href = n.getAttribute("href");
|
||
if(fill)
|
||
{
|
||
this.setColor(fill);
|
||
this.setRectFilled([conv(x,y),conv(width,height,false)]);
|
||
}
|
||
if(href) await this.drawSvg(href, conv(width,height,false), conv(x,y));
|
||
if(stroke)
|
||
{
|
||
var strokeWidth = parseInt(style.strokeWidth)||0;
|
||
this.options.pelSize = new Point(strokeWidth/viewbox[2], strokeWidth/viewbox[3]);
|
||
this.setDomain();
|
||
this.setColor(stroke);
|
||
this.setRectOutlined([conv(x,y),conv(width,height,false)]);
|
||
}
|
||
} break;
|
||
case "script": {
|
||
if(n.textContent)
|
||
{
|
||
var fun = new Function("window", "document", "naplps", n.textContent);
|
||
fun(win, node, this);
|
||
}
|
||
} break;
|
||
default: break;
|
||
}
|
||
if(n.children.length > 0)
|
||
await drawChildren(n.children);
|
||
}
|
||
}
|
||
if(node.children.length > 0)
|
||
await drawChildren(node.children);
|
||
}
|
||
}
|
||
|
||
export class Telidon {
|
||
server: net.Server;
|
||
connectionListener: (c: Naplps) => void;
|
||
constructor(options: net.ServerOpts | undefined, connectionListener: (c: Naplps) => void) {
|
||
var domain = Domain.create();
|
||
this.connectionListener = connectionListener;
|
||
domain.run(_=>{
|
||
this.server = net.createServer(options, socket=>{
|
||
var dom = Domain.create();
|
||
var naplps = new Naplps(socket);
|
||
dom.add(socket);
|
||
dom.add(naplps);
|
||
dom.on("error", err=>naplps.emit("error", err));
|
||
dom.run(this.connectionListener, naplps);
|
||
});
|
||
});
|
||
}
|
||
|
||
listen(...args: number[]) {
|
||
this.server.listen(...args);
|
||
}
|
||
} |