diff --git a/.gitignore b/.gitignore index bf0061c..d610514 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ node_modules/ lib3/ lib6/ coverage/ -protocol/* -!protocol/*.proto +**/protocol/* +!**/protocol/*.proto .nyc_output/ diff --git a/examples/painter/Makefile b/examples/painter/Makefile index d1f94b7..2471236 100644 --- a/examples/painter/Makefile +++ b/examples/painter/Makefile @@ -3,21 +3,22 @@ SHELL := /bin/bash PATH := ./node_modules/.bin:$(PATH) .PHONY: preview -preview: node_modules proto - node server.js & - wintersmith preview +preview: node_modules protocol/service.d.ts protocol/service.js + wintersmith preview --chdir client -.PHONY: proto -proto: node_modules - pbjs -t static-module -w commonjs service.proto -o contents/protocol.js - pbts -o contents/protocol.d.ts contents/protocol.js +protocol/service.js: node_modules protocol/service.proto + pbjs -t static-module -w commonjs protocol/service.proto -o protocol/service.js + +protocol/service.d.ts: node_modules protocol/service.js + pbts -o protocol/service.d.ts protocol/service.js node_modules: npm install .PHONY: clean clean: - rm -f contents/protocol.js + rm -f protocol/service.js + rm -f protocol/service.d.ts .PHONY: distclean distclean: clean diff --git a/examples/painter/canvas.png b/examples/painter/canvas.png new file mode 100644 index 0000000..4db9ee7 Binary files /dev/null and b/examples/painter/canvas.png differ diff --git a/examples/painter/client/canvas.js b/examples/painter/client/canvas.js new file mode 100644 index 0000000..ccbdcdf --- /dev/null +++ b/examples/painter/client/canvas.js @@ -0,0 +1 @@ +module.exports = window diff --git a/examples/painter/config.json b/examples/painter/client/config.json similarity index 90% rename from examples/painter/config.json rename to examples/painter/client/config.json index 105026a..553abfe 100644 --- a/examples/painter/config.json +++ b/examples/painter/client/config.json @@ -1,5 +1,6 @@ { "browserify": { + "watchify": false, "extensions": [".js", ".ts"], "plugins": ["tsify"] }, diff --git a/examples/painter/contents/index.json b/examples/painter/client/contents/index.json similarity index 100% rename from examples/painter/contents/index.json rename to examples/painter/client/contents/index.json diff --git a/examples/painter/client/contents/paint.ts b/examples/painter/client/contents/paint.ts new file mode 100644 index 0000000..9ffa5fd --- /dev/null +++ b/examples/painter/client/contents/paint.ts @@ -0,0 +1,250 @@ + +import {Client} from 'wsrpc' +import {Painter, PaintEvent, StatusEvent} from './../../protocol/service' +import * as zlib from 'browserify-zlib-next' +import * as shared from './../../shared/paint' + +interface Position { + x: number + y: number + timestamp: number +} + +interface DrawEvent { + pos: Position + lastPos?: Position + color: number +} + +const colors = [ + 0x467588, + 0xFFFFFF, + 0xFCE5BC, + 0xFDCD92, + 0xFCAC96, + 0xDD8193, +] + +function randomColor() { + return colors[Math.floor(colors.length * Math.random())] +} + +let now: () => number +if (window.performance) { + now = () => window.performance.now() +} else { + now = () => Date.now() +} + +const client = new Client('ws://192.168.1.33:4242', Painter, { + sendTimeout: 5000, + eventTypes: { + paint: PaintEvent, + status: StatusEvent, + } +}) + +client.on('open', () => { + document.documentElement.classList.add('connected') +}) + +client.on('close', () => { + document.documentElement.classList.remove('connected') +}) + +window.addEventListener('DOMContentLoaded', async () => { + const status = document.createElement('div') + status.className = 'status' + status.innerHTML = 'Connecting...' + document.body.appendChild(status) + + client.on('event status', (event: StatusEvent) => { + status.innerHTML = `Users: ${ event.users }` + }) + + client.on('close', () => { + status.innerHTML = 'Disconnected' + }) + + client.on('error', (error) => { + console.warn('client error', error) + }) + + let activeColor: number = colors[0] + + const colorWells: HTMLSpanElement[] = [] + const colorPicker = document.createElement('div') + colorPicker.className = 'picker' + for (const color of colors) { + const well = document.createElement('span') + const cssColor = '#' + color.toString(16) + well.style.backgroundColor = cssColor + well.style.outlineColor = cssColor + well.addEventListener('click', (event) => { + event.preventDefault() + colorWells.forEach((el) => el.classList.remove('active')) + well.classList.add('active') + activeColor = color + console.log(activeColor) + }) + colorWells.push(well) + colorPicker.appendChild(well) + } + document.body.appendChild(colorPicker) + + colorWells[0].classList.add('active') + + const canvas = document.querySelector('canvas') + const ctx = canvas.getContext('2d') + + canvas.width = window.innerWidth + canvas.height = window.innerHeight + + client.on('event paint', (event: PaintEvent) => { + shared.paint(event, ctx) + }) + + async function fetchCanvas() { + const request = { + width: Math.min(window.innerWidth, 2048), + height: Math.min(window.innerHeight, 2048), + } + console.log('loading canvas...', request) + const response = await client.service.getCanvas(request) + + console.log(`response size: ${ ~~(response.image.length / 1024) }kb`) + + const arr = response.image + let buffer = Buffer.from(arr.buffer) + buffer = buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength) + + const data = await new Promise((resolve, reject) => { + zlib.gunzip(buffer, (error, result) => { + if (error) { reject(error) } else { resolve(result) } + }) + }) + + console.log(`decompressed: ${ ~~(data.length / 1024) }kb`) + + const imageData = ctx.createImageData(request.width, request.height) + imageData.data.set(new Uint8ClampedArray(data.buffer)) + ctx.putImageData(imageData, 0, 0) + } + + let debounceTimer + window.addEventListener('resize', () => { + if (window.innerWidth <= canvas.width && window.innerHeight <= canvas.height) { + const data = ctx.getImageData(0, 0, canvas.width, canvas.height) + canvas.width = window.innerWidth + canvas.height = window.innerHeight + ctx.putImageData(data, 0, 0) + } else { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + clearTimeout(debounceTimer) + setTimeout(fetchCanvas, 1000) + } + }) + + await fetchCanvas() + + function draw(event: DrawEvent) { + let velocity = 0 + if (event.lastPos) { + const dx = event.lastPos.x - event.pos.x + const dy = event.lastPos.y - event.pos.y + const dt = event.pos.timestamp - event.lastPos.timestamp + velocity = Math.sqrt(dx*dx + dy*dy) / dt + } + client.service.paint({ + x: event.pos.x, + y: event.pos.y, + color: event.color, + size: 20 + velocity * 20, + }).catch((error: Error) => { + console.warn('error drawing', error.message) + }) + } + + let mouseDraw: DrawEvent|undefined + + canvas.addEventListener('mousedown', (event) => { + mouseDraw = { + pos: { + x: event.x, + y: event.y, + timestamp: event.timeStamp || now(), + }, + color: activeColor, + } + draw(mouseDraw) + event.preventDefault() + }) + + canvas.addEventListener('mousemove', (event) => { + if (mouseDraw) { + mouseDraw.lastPos = mouseDraw.pos + mouseDraw.pos = { + x: event.x, + y: event.y, + timestamp: event.timeStamp || now(), + } + draw(mouseDraw) + } + }) + + const mouseup = (event) => { + mouseDraw = undefined + } + canvas.addEventListener('mouseup', mouseup) + canvas.addEventListener('mouseleave', mouseup) + + let fingerDraw: {[id: number]: DrawEvent} = {} + + canvas.addEventListener('touchstart', (event) => { + for (var i = 0; i < event.touches.length; i++) { + const touch = event.touches[i] + fingerDraw[touch.identifier] = { + pos: { + x: touch.screenX, + y: touch.screenY, + timestamp: event.timeStamp || now(), + }, + color: activeColor + } + draw(fingerDraw[touch.identifier]) + } + event.preventDefault() + }) + + canvas.addEventListener('touchmove', (event) => { + for (var i = 0; i < event.touches.length; i++) { + const touch = event.touches[i] + const drawEvent = fingerDraw[touch.identifier] + if (drawEvent) { + drawEvent.lastPos = drawEvent.pos + drawEvent.pos = { + x: touch.screenX, + y: touch.screenY, + timestamp: event.timeStamp || now(), + } + draw(drawEvent) + } + } + event.preventDefault() + }) + + const touchend = (event: TouchEvent) => { + for (var i = 0; i < event.touches.length; i++) { + const touch = event.touches[i] + delete fingerDraw[touch.identifier] + } + event.preventDefault() + } + canvas.addEventListener('touchend', touchend) + canvas.addEventListener('touchcancel', touchend) +}) + +console.log(' ;-) ') +window['colors'] = colors +window['client'] = client diff --git a/examples/painter/contents/style.css b/examples/painter/client/contents/style.css similarity index 61% rename from examples/painter/contents/style.css rename to examples/painter/client/contents/style.css index 81da618..a67e94a 100644 --- a/examples/painter/contents/style.css +++ b/examples/painter/client/contents/style.css @@ -2,18 +2,19 @@ * { margin: 0; padding: 0; border: 0; line-height: 1; user-select: none; -webkit-user-select: none; + box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif; font-size: 14px; font-weight: 300; - background: #0b1215; + background: black; color: #f5f5f5; } .connected body { - background: #467588; + background: #ffffff; } a { @@ -21,6 +22,7 @@ a { } a, .status { + background: rgba(0, 0, 0, 0.45); position: fixed; top: 0; right: 0; @@ -42,4 +44,22 @@ canvas { position: absolute; top: 0; left: 0; +} + +.picker { + position: absolute; + bottom: 0; +} + +.picker span { + display: block; + width: 5vh; + height: 5vh; + position: relative; +} + +.picker span.active { + outline: 0.2em solid white; + border-left: 0.2em solid black; + z-index: 1; } \ No newline at end of file diff --git a/examples/painter/templates/index.html b/examples/painter/client/templates/index.html similarity index 100% rename from examples/painter/templates/index.html rename to examples/painter/client/templates/index.html diff --git a/examples/painter/contents/paint.ts b/examples/painter/contents/paint.ts deleted file mode 100644 index 211f165..0000000 --- a/examples/painter/contents/paint.ts +++ /dev/null @@ -1,191 +0,0 @@ - -import {Client} from 'wsrpc' -import {Painter, PaintMessage, StatusMessage} from './protocol' - -interface Position { - x: number - y: number - timestamp: number -} - -interface PaintEvent { - pos: Position - lastPos?: Position - color: number -} - -const colors = [ - 0xFCE5BC, - 0xFDCD92, - 0xFCAC96, - 0xDD8193, -] - -function randomColor() { - return colors[Math.floor(colors.length * Math.random())] -} - -let now: () => number -if (window.performance) { - now = () => window.performance.now() -} else { - now = () => Date.now() -} - -const client = new Client('ws://192.168.1.33:4242', Painter, { - sendTimeout: 500, - eventTypes: { - paint: PaintMessage, - status: StatusMessage, - } -}) - -client.on('open', () => { - document.documentElement.classList.add('connected') -}) - -client.on('close', () => { - document.documentElement.classList.remove('connected') -}) - -window.addEventListener('DOMContentLoaded', () => { - const status = document.createElement('div') - status.className = 'status' - status.innerHTML = 'Connecting...' - document.body.appendChild(status) - - client.on('event status', (msg: StatusMessage) => { - status.innerHTML = `Users: ${ msg.users }` - }) - - client.on('close', () => { - status.innerHTML = 'Disconnected' - }) - - client.on('error', (error) => { - console.warn('client error', error) - }) - - const canvas = document.querySelector('canvas') - const ctx = canvas.getContext('2d') - const ratio = window.devicePixelRatio || 1 - - canvas.width = window.innerWidth * ratio - canvas.height = window.innerHeight * ratio - - window.addEventListener('resize', () => { - const data = ctx.getImageData(0, 0, canvas.width, canvas.height) - canvas.width = window.innerWidth * ratio - canvas.height = window.innerHeight * ratio - ctx.putImageData(data, 0, 0) - }) - - const vMax = 10 - function paint(event: PaintEvent) { - let velocity = 0 - if (event.lastPos) { - const dx = event.lastPos.x - event.pos.x - const dy = event.lastPos.y - event.pos.y - const dt = event.pos.timestamp - event.lastPos.timestamp - velocity = Math.sqrt(dx*dx + dy*dy) / dt - } - client.service.paint({ - x: event.pos.x, - y: event.pos.y, - color: event.color, - size: 2 + Math.min(velocity * 4, 8), - }) - console.log(velocity) - } - - client.on('event paint', (p: PaintMessage) => { - ctx.beginPath() - ctx.moveTo(p.x * ratio, p.y * ratio) - ctx.ellipse(p.x * ratio, p.y * ratio, p.size * ratio, p.size * ratio, 0, 0, Math.PI*2) - ctx.fillStyle = '#' + p.color.toString(16) - ctx.closePath() - ctx.fill() - }) - - let mousePaint: PaintEvent|undefined - - canvas.addEventListener('mousedown', (event) => { - mousePaint = { - pos: { - x: event.x, - y: event.y, - timestamp: event.timeStamp || now(), - }, - color: randomColor(), - } - paint(mousePaint) - event.preventDefault() - }) - - canvas.addEventListener('mousemove', (event) => { - if (mousePaint) { - mousePaint.lastPos = mousePaint.pos - mousePaint.pos = { - x: event.x, - y: event.y, - timestamp: event.timeStamp || now(), - } - paint(mousePaint) - } - }) - - const mouseup = (event) => { - mousePaint = undefined - } - canvas.addEventListener('mouseup', mouseup) - canvas.addEventListener('mouseleave', mouseup) - - let fingerPaint: {[id: number]: PaintEvent} = {} - - canvas.addEventListener('touchstart', (event) => { - for (var i = 0; i < event.touches.length; i++) { - const touch = event.touches[i] - fingerPaint[touch.identifier] = { - pos: { - x: touch.screenX, - y: touch.screenY, - timestamp: event.timeStamp || now(), - }, - color: randomColor() - } - paint(fingerPaint[touch.identifier]) - } - event.preventDefault() - }) - - canvas.addEventListener('touchmove', (event) => { - for (var i = 0; i < event.touches.length; i++) { - const touch = event.touches[i] - const paintEvent = fingerPaint[touch.identifier] - if (paintEvent) { - paintEvent.lastPos = paintEvent.pos - paintEvent.pos = { - x: touch.screenX, - y: touch.screenY, - timestamp: event.timeStamp || now(), - } - paint(paintEvent) - } - } - event.preventDefault() - }) - - const touchend = (event: TouchEvent) => { - for (var i = 0; i < event.touches.length; i++) { - const touch = event.touches[i] - delete fingerPaint[touch.identifier] - } - event.preventDefault() - } - canvas.addEventListener('touchend', touchend) - canvas.addEventListener('touchcancel', touchend) -}) - -console.log(' ;-) ') -window['colors'] = colors -window['client'] = client diff --git a/examples/painter/contents/protocol.d.ts b/examples/painter/contents/protocol.d.ts deleted file mode 100644 index 26b5555..0000000 --- a/examples/painter/contents/protocol.d.ts +++ /dev/null @@ -1,328 +0,0 @@ -import * as $protobuf from "protobufjs"; - -/** Represents a Painter */ -export class Painter extends $protobuf.rpc.Service { - - /** - * Constructs a new Painter service. - * @param rpcImpl RPC implementation - * @param [requestDelimited=false] Whether requests are length-delimited - * @param [responseDelimited=false] Whether responses are length-delimited - */ - constructor(rpcImpl: $protobuf.RPCImpl, requestDelimited?: boolean, responseDelimited?: boolean); - - /** - * Creates new Painter service using the specified rpc implementation. - * @param rpcImpl RPC implementation - * @param [requestDelimited=false] Whether requests are length-delimited - * @param [responseDelimited=false] Whether responses are length-delimited - * @returns RPC service. Useful where requests and/or responses are streamed. - */ - public static create(rpcImpl: $protobuf.RPCImpl, requestDelimited?: boolean, responseDelimited?: boolean): Painter; - - /** - * Calls Paint. - * @param request PaintMessage message or plain object - * @param callback Node-style callback called with the error, if any, and Empty - */ - public paint(request: IPaintMessage, callback: Painter.PaintCallback): void; - - /** - * Calls Paint. - * @param request PaintMessage message or plain object - * @returns Promise - */ - public paint(request: IPaintMessage): Promise; -} - -export namespace Painter { - - /** - * Callback as used by {@link Painter#paint}. - * @param error Error, if any - * @param [response] Empty - */ - type PaintCallback = (error: (Error|null), response?: Empty) => void; -} - -/** Properties of an Empty. */ -export interface IEmpty { -} - -/** Represents an Empty. */ -export class Empty { - - /** - * Constructs a new Empty. - * @param [properties] Properties to set - */ - constructor(properties?: IEmpty); - - /** - * Creates a new Empty instance using the specified properties. - * @param [properties] Properties to set - * @returns Empty instance - */ - public static create(properties?: IEmpty): Empty; - - /** - * Encodes the specified Empty message. Does not implicitly {@link Empty.verify|verify} messages. - * @param message Empty message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IEmpty, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified Empty message, length delimited. Does not implicitly {@link Empty.verify|verify} messages. - * @param message Empty message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IEmpty, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes an Empty message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns Empty - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): Empty; - - /** - * Decodes an Empty message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns Empty - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): Empty; - - /** - * Verifies an Empty message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string|null); - - /** - * Creates an Empty message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns Empty - */ - public static fromObject(object: { [k: string]: any }): Empty; - - /** - * Creates a plain object from an Empty message. Also converts values to other types if specified. - * @param message Empty - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: Empty, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this Empty to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -} - -/** Properties of a PaintMessage. */ -export interface IPaintMessage { - - /** PaintMessage x */ - x: number; - - /** PaintMessage y */ - y: number; - - /** PaintMessage size */ - size: number; - - /** PaintMessage color */ - color: number; -} - -/** Represents a PaintMessage. */ -export class PaintMessage { - - /** - * Constructs a new PaintMessage. - * @param [properties] Properties to set - */ - constructor(properties?: IPaintMessage); - - /** PaintMessage x. */ - public x: number; - - /** PaintMessage y. */ - public y: number; - - /** PaintMessage size. */ - public size: number; - - /** PaintMessage color. */ - public color: number; - - /** - * Creates a new PaintMessage instance using the specified properties. - * @param [properties] Properties to set - * @returns PaintMessage instance - */ - public static create(properties?: IPaintMessage): PaintMessage; - - /** - * Encodes the specified PaintMessage message. Does not implicitly {@link PaintMessage.verify|verify} messages. - * @param message PaintMessage message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IPaintMessage, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified PaintMessage message, length delimited. Does not implicitly {@link PaintMessage.verify|verify} messages. - * @param message PaintMessage message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IPaintMessage, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a PaintMessage message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns PaintMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): PaintMessage; - - /** - * Decodes a PaintMessage message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns PaintMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): PaintMessage; - - /** - * Verifies a PaintMessage message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string|null); - - /** - * Creates a PaintMessage message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns PaintMessage - */ - public static fromObject(object: { [k: string]: any }): PaintMessage; - - /** - * Creates a plain object from a PaintMessage message. Also converts values to other types if specified. - * @param message PaintMessage - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: PaintMessage, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this PaintMessage to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -} - -/** Properties of a StatusMessage. */ -export interface IStatusMessage { - - /** StatusMessage users */ - users: number; -} - -/** Represents a StatusMessage. */ -export class StatusMessage { - - /** - * Constructs a new StatusMessage. - * @param [properties] Properties to set - */ - constructor(properties?: IStatusMessage); - - /** StatusMessage users. */ - public users: number; - - /** - * Creates a new StatusMessage instance using the specified properties. - * @param [properties] Properties to set - * @returns StatusMessage instance - */ - public static create(properties?: IStatusMessage): StatusMessage; - - /** - * Encodes the specified StatusMessage message. Does not implicitly {@link StatusMessage.verify|verify} messages. - * @param message StatusMessage message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encode(message: IStatusMessage, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Encodes the specified StatusMessage message, length delimited. Does not implicitly {@link StatusMessage.verify|verify} messages. - * @param message StatusMessage message or plain object to encode - * @param [writer] Writer to encode to - * @returns Writer - */ - public static encodeDelimited(message: IStatusMessage, writer?: $protobuf.Writer): $protobuf.Writer; - - /** - * Decodes a StatusMessage message from the specified reader or buffer. - * @param reader Reader or buffer to decode from - * @param [length] Message length if known beforehand - * @returns StatusMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): StatusMessage; - - /** - * Decodes a StatusMessage message from the specified reader or buffer, length delimited. - * @param reader Reader or buffer to decode from - * @returns StatusMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): StatusMessage; - - /** - * Verifies a StatusMessage message. - * @param message Plain object to verify - * @returns `null` if valid, otherwise the reason why it is not - */ - public static verify(message: { [k: string]: any }): (string|null); - - /** - * Creates a StatusMessage message from a plain object. Also converts values to their respective internal types. - * @param object Plain object - * @returns StatusMessage - */ - public static fromObject(object: { [k: string]: any }): StatusMessage; - - /** - * Creates a plain object from a StatusMessage message. Also converts values to other types if specified. - * @param message StatusMessage - * @param [options] Conversion options - * @returns Plain object - */ - public static toObject(message: StatusMessage, options?: $protobuf.IConversionOptions): { [k: string]: any }; - - /** - * Converts this StatusMessage to JSON. - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -} diff --git a/examples/painter/contents/protocol.js b/examples/painter/contents/protocol.js deleted file mode 100644 index 33c8a38..0000000 --- a/examples/painter/contents/protocol.js +++ /dev/null @@ -1,579 +0,0 @@ -/*eslint-disable block-scoped-var, no-redeclare, no-control-regex, no-prototype-builtins*/ -"use strict"; - -var $protobuf = require("protobufjs/minimal"); - -// Common aliases -var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; - -// Exported root namespace -var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); - -$root.Painter = (function() { - - /** - * Constructs a new Painter service. - * @exports Painter - * @classdesc Represents a Painter - * @extends $protobuf.rpc.Service - * @constructor - * @param {$protobuf.RPCImpl} rpcImpl RPC implementation - * @param {boolean} [requestDelimited=false] Whether requests are length-delimited - * @param {boolean} [responseDelimited=false] Whether responses are length-delimited - */ - function Painter(rpcImpl, requestDelimited, responseDelimited) { - $protobuf.rpc.Service.call(this, rpcImpl, requestDelimited, responseDelimited); - } - - (Painter.prototype = Object.create($protobuf.rpc.Service.prototype)).constructor = Painter; - - /** - * Creates new Painter service using the specified rpc implementation. - * @param {$protobuf.RPCImpl} rpcImpl RPC implementation - * @param {boolean} [requestDelimited=false] Whether requests are length-delimited - * @param {boolean} [responseDelimited=false] Whether responses are length-delimited - * @returns {Painter} RPC service. Useful where requests and/or responses are streamed. - */ - Painter.create = function create(rpcImpl, requestDelimited, responseDelimited) { - return new this(rpcImpl, requestDelimited, responseDelimited); - }; - - /** - * Callback as used by {@link Painter#paint}. - * @memberof Painter - * @typedef PaintCallback - * @type {function} - * @param {Error|null} error Error, if any - * @param {Empty} [response] Empty - */ - - /** - * Calls Paint. - * @param {IPaintMessage} request PaintMessage message or plain object - * @param {Painter.PaintCallback} callback Node-style callback called with the error, if any, and Empty - * @returns {undefined} - */ - Painter.prototype.paint = function paint(request, callback) { - return this.rpcCall(paint, $root.PaintMessage, $root.Empty, request, callback); - }; - - /** - * Calls Paint. - * @memberof Painter.prototype - * @function paint - * @param {IPaintMessage} request PaintMessage message or plain object - * @returns {Promise} Promise - * @variation 2 - */ - - return Painter; -})(); - -$root.Empty = (function() { - - /** - * Properties of an Empty. - * @exports IEmpty - * @interface IEmpty - */ - - /** - * Constructs a new Empty. - * @exports Empty - * @classdesc Represents an Empty. - * @constructor - * @param {IEmpty=} [properties] Properties to set - */ - function Empty(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * Creates a new Empty instance using the specified properties. - * @param {IEmpty=} [properties] Properties to set - * @returns {Empty} Empty instance - */ - Empty.create = function create(properties) { - return new Empty(properties); - }; - - /** - * Encodes the specified Empty message. Does not implicitly {@link Empty.verify|verify} messages. - * @param {IEmpty} message Empty message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - Empty.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - return writer; - }; - - /** - * Encodes the specified Empty message, length delimited. Does not implicitly {@link Empty.verify|verify} messages. - * @param {IEmpty} message Empty message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - Empty.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes an Empty message from the specified reader or buffer. - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {Empty} Empty - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - Empty.decode = function decode(reader, length) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.Empty(); - while (reader.pos < end) { - var tag = reader.uint32(); - switch (tag >>> 3) { - default: - reader.skipType(tag & 7); - break; - } - } - return message; - }; - - /** - * Decodes an Empty message from the specified reader or buffer, length delimited. - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {Empty} Empty - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - Empty.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies an Empty message. - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - Empty.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - return null; - }; - - /** - * Creates an Empty message from a plain object. Also converts values to their respective internal types. - * @param {Object.} object Plain object - * @returns {Empty} Empty - */ - Empty.fromObject = function fromObject(object) { - if (object instanceof $root.Empty) - return object; - return new $root.Empty(); - }; - - /** - * Creates a plain object from an Empty message. Also converts values to other types if specified. - * @param {Empty} message Empty - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - Empty.toObject = function toObject() { - return {}; - }; - - /** - * Converts this Empty to JSON. - * @returns {Object.} JSON object - */ - Empty.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - return Empty; -})(); - -$root.PaintMessage = (function() { - - /** - * Properties of a PaintMessage. - * @exports IPaintMessage - * @interface IPaintMessage - * @property {number} x PaintMessage x - * @property {number} y PaintMessage y - * @property {number} size PaintMessage size - * @property {number} color PaintMessage color - */ - - /** - * Constructs a new PaintMessage. - * @exports PaintMessage - * @classdesc Represents a PaintMessage. - * @constructor - * @param {IPaintMessage=} [properties] Properties to set - */ - function PaintMessage(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * PaintMessage x. - * @type {number} - */ - PaintMessage.prototype.x = 0; - - /** - * PaintMessage y. - * @type {number} - */ - PaintMessage.prototype.y = 0; - - /** - * PaintMessage size. - * @type {number} - */ - PaintMessage.prototype.size = 0; - - /** - * PaintMessage color. - * @type {number} - */ - PaintMessage.prototype.color = 0; - - /** - * Creates a new PaintMessage instance using the specified properties. - * @param {IPaintMessage=} [properties] Properties to set - * @returns {PaintMessage} PaintMessage instance - */ - PaintMessage.create = function create(properties) { - return new PaintMessage(properties); - }; - - /** - * Encodes the specified PaintMessage message. Does not implicitly {@link PaintMessage.verify|verify} messages. - * @param {IPaintMessage} message PaintMessage message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - PaintMessage.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - writer.uint32(/* id 1, wireType 5 =*/13).float(message.x); - writer.uint32(/* id 2, wireType 5 =*/21).float(message.y); - writer.uint32(/* id 3, wireType 5 =*/29).float(message.size); - writer.uint32(/* id 4, wireType 0 =*/32).int32(message.color); - return writer; - }; - - /** - * Encodes the specified PaintMessage message, length delimited. Does not implicitly {@link PaintMessage.verify|verify} messages. - * @param {IPaintMessage} message PaintMessage message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - PaintMessage.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a PaintMessage message from the specified reader or buffer. - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {PaintMessage} PaintMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - PaintMessage.decode = function decode(reader, length) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.PaintMessage(); - while (reader.pos < end) { - var tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - message.x = reader.float(); - break; - case 2: - message.y = reader.float(); - break; - case 3: - message.size = reader.float(); - break; - case 4: - message.color = reader.int32(); - break; - default: - reader.skipType(tag & 7); - break; - } - } - if (!message.hasOwnProperty("x")) - throw $util.ProtocolError("missing required 'x'", { instance: message }); - if (!message.hasOwnProperty("y")) - throw $util.ProtocolError("missing required 'y'", { instance: message }); - if (!message.hasOwnProperty("size")) - throw $util.ProtocolError("missing required 'size'", { instance: message }); - if (!message.hasOwnProperty("color")) - throw $util.ProtocolError("missing required 'color'", { instance: message }); - return message; - }; - - /** - * Decodes a PaintMessage message from the specified reader or buffer, length delimited. - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {PaintMessage} PaintMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - PaintMessage.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a PaintMessage message. - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - PaintMessage.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (typeof message.x !== "number") - return "x: number expected"; - if (typeof message.y !== "number") - return "y: number expected"; - if (typeof message.size !== "number") - return "size: number expected"; - if (!$util.isInteger(message.color)) - return "color: integer expected"; - return null; - }; - - /** - * Creates a PaintMessage message from a plain object. Also converts values to their respective internal types. - * @param {Object.} object Plain object - * @returns {PaintMessage} PaintMessage - */ - PaintMessage.fromObject = function fromObject(object) { - if (object instanceof $root.PaintMessage) - return object; - var message = new $root.PaintMessage(); - if (object.x != null) - message.x = Number(object.x); - if (object.y != null) - message.y = Number(object.y); - if (object.size != null) - message.size = Number(object.size); - if (object.color != null) - message.color = object.color | 0; - return message; - }; - - /** - * Creates a plain object from a PaintMessage message. Also converts values to other types if specified. - * @param {PaintMessage} message PaintMessage - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - PaintMessage.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) { - object.x = 0; - object.y = 0; - object.size = 0; - object.color = 0; - } - if (message.x != null && message.hasOwnProperty("x")) - object.x = options.json && !isFinite(message.x) ? String(message.x) : message.x; - if (message.y != null && message.hasOwnProperty("y")) - object.y = options.json && !isFinite(message.y) ? String(message.y) : message.y; - if (message.size != null && message.hasOwnProperty("size")) - object.size = options.json && !isFinite(message.size) ? String(message.size) : message.size; - if (message.color != null && message.hasOwnProperty("color")) - object.color = message.color; - return object; - }; - - /** - * Converts this PaintMessage to JSON. - * @returns {Object.} JSON object - */ - PaintMessage.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - return PaintMessage; -})(); - -$root.StatusMessage = (function() { - - /** - * Properties of a StatusMessage. - * @exports IStatusMessage - * @interface IStatusMessage - * @property {number} users StatusMessage users - */ - - /** - * Constructs a new StatusMessage. - * @exports StatusMessage - * @classdesc Represents a StatusMessage. - * @constructor - * @param {IStatusMessage=} [properties] Properties to set - */ - function StatusMessage(properties) { - if (properties) - for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) - if (properties[keys[i]] != null) - this[keys[i]] = properties[keys[i]]; - } - - /** - * StatusMessage users. - * @type {number} - */ - StatusMessage.prototype.users = 0; - - /** - * Creates a new StatusMessage instance using the specified properties. - * @param {IStatusMessage=} [properties] Properties to set - * @returns {StatusMessage} StatusMessage instance - */ - StatusMessage.create = function create(properties) { - return new StatusMessage(properties); - }; - - /** - * Encodes the specified StatusMessage message. Does not implicitly {@link StatusMessage.verify|verify} messages. - * @param {IStatusMessage} message StatusMessage message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - StatusMessage.encode = function encode(message, writer) { - if (!writer) - writer = $Writer.create(); - writer.uint32(/* id 1, wireType 0 =*/8).int32(message.users); - return writer; - }; - - /** - * Encodes the specified StatusMessage message, length delimited. Does not implicitly {@link StatusMessage.verify|verify} messages. - * @param {IStatusMessage} message StatusMessage message or plain object to encode - * @param {$protobuf.Writer} [writer] Writer to encode to - * @returns {$protobuf.Writer} Writer - */ - StatusMessage.encodeDelimited = function encodeDelimited(message, writer) { - return this.encode(message, writer).ldelim(); - }; - - /** - * Decodes a StatusMessage message from the specified reader or buffer. - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @param {number} [length] Message length if known beforehand - * @returns {StatusMessage} StatusMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - StatusMessage.decode = function decode(reader, length) { - if (!(reader instanceof $Reader)) - reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.StatusMessage(); - while (reader.pos < end) { - var tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - message.users = reader.int32(); - break; - default: - reader.skipType(tag & 7); - break; - } - } - if (!message.hasOwnProperty("users")) - throw $util.ProtocolError("missing required 'users'", { instance: message }); - return message; - }; - - /** - * Decodes a StatusMessage message from the specified reader or buffer, length delimited. - * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from - * @returns {StatusMessage} StatusMessage - * @throws {Error} If the payload is not a reader or valid buffer - * @throws {$protobuf.util.ProtocolError} If required fields are missing - */ - StatusMessage.decodeDelimited = function decodeDelimited(reader) { - if (!(reader instanceof $Reader)) - reader = $Reader(reader); - return this.decode(reader, reader.uint32()); - }; - - /** - * Verifies a StatusMessage message. - * @param {Object.} message Plain object to verify - * @returns {string|null} `null` if valid, otherwise the reason why it is not - */ - StatusMessage.verify = function verify(message) { - if (typeof message !== "object" || message === null) - return "object expected"; - if (!$util.isInteger(message.users)) - return "users: integer expected"; - return null; - }; - - /** - * Creates a StatusMessage message from a plain object. Also converts values to their respective internal types. - * @param {Object.} object Plain object - * @returns {StatusMessage} StatusMessage - */ - StatusMessage.fromObject = function fromObject(object) { - if (object instanceof $root.StatusMessage) - return object; - var message = new $root.StatusMessage(); - if (object.users != null) - message.users = object.users | 0; - return message; - }; - - /** - * Creates a plain object from a StatusMessage message. Also converts values to other types if specified. - * @param {StatusMessage} message StatusMessage - * @param {$protobuf.IConversionOptions} [options] Conversion options - * @returns {Object.} Plain object - */ - StatusMessage.toObject = function toObject(message, options) { - if (!options) - options = {}; - var object = {}; - if (options.defaults) - object.users = 0; - if (message.users != null && message.hasOwnProperty("users")) - object.users = message.users; - return object; - }; - - /** - * Converts this StatusMessage to JSON. - * @returns {Object.} JSON object - */ - StatusMessage.prototype.toJSON = function toJSON() { - return this.constructor.toObject(this, $protobuf.util.toJSONOptions); - }; - - return StatusMessage; -})(); - -module.exports = $root; diff --git a/examples/painter/package.json b/examples/painter/package.json index 01c9ae6..acbcd33 100644 --- a/examples/painter/package.json +++ b/examples/painter/package.json @@ -1,8 +1,15 @@ { "name": "wsrpc-example-painter", "private": true, + "browser": { + "canvas": "./client/canvas.js" + }, "dependencies": { + "browserify-zlib-next": "^1.0.1", + "canvas": "^1.6.5", + "node-dev": "^3.1.3", "protobufjs": "github:dcodeio/protobuf.js#3a95945", + "ts-node": "^3.0.2", "tsify": "^3.0.1", "typescript": "^2.3.1", "wintersmith": "^2.3.6", diff --git a/examples/painter/protocol/service.proto b/examples/painter/protocol/service.proto new file mode 100644 index 0000000..5177c4c --- /dev/null +++ b/examples/painter/protocol/service.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +service Painter { + rpc GetCanvas (CanvasRequest) returns (CanvasResponse) {} + rpc Paint (PaintEvent) returns (Empty) {} +} + +message Empty {} + +message PaintEvent { + required float x = 1; + required float y = 2; + required float size = 3; + required int32 color = 4; +} + +message StatusEvent { + required int32 users = 1; +} + +message CanvasRequest { + required int32 width = 1; + required int32 height = 2; +} + +message CanvasResponse { + required bytes image = 1; +} diff --git a/examples/painter/server.js b/examples/painter/server.js deleted file mode 100644 index 34cb257..0000000 --- a/examples/painter/server.js +++ /dev/null @@ -1,27 +0,0 @@ - -const wsrpc = require('wsrpc') -const protobuf = require('protobufjs') - -const proto = protobuf.loadSync(`${ __dirname }/service.proto`) -const PaintMessage = proto.lookupType('PaintMessage') -const StatusMessage = proto.lookupType('StatusMessage') - -const server = new wsrpc.Server({ - port: 4242, - service: proto.lookupService('Painter') -}) - -server.implement('paint', (message) => { - server.broadcast('paint', PaintMessage.encode(message).finish()) - return Promise.resolve({}) -}) - -const broadcastStatus = () => { - const data = StatusMessage.encode({users: server.connections.length}).finish() - server.broadcast('status', data) -} - -server.on('connection', (connection) => { - broadcastStatus() - connection.once('close', broadcastStatus) -}) diff --git a/examples/painter/server/server.ts b/examples/painter/server/server.ts new file mode 100644 index 0000000..5dcca85 --- /dev/null +++ b/examples/painter/server/server.ts @@ -0,0 +1,81 @@ + +const wsrpc = require('wsrpc') +const protobuf = require('protobufjs') +const Canvas = require('canvas') +const zlib = require('zlib') + +import * as fs from 'fs' + +import {PaintEvent, StatusEvent, CanvasRequest} from './../protocol/service' +import * as shared from './../shared/paint' + +const proto = protobuf.loadSync(`${ __dirname }/../protocol/service.proto`) + +const width = 2024 +const height = 2024 + +const canvas = new Canvas() +canvas.width = width +canvas.height = height + +const ctx = canvas.getContext('2d') + +try { + const img = new Canvas.Image() + img.src = fs.readFileSync('canvas.png') + ctx.drawImage(img, 0, 0) +} catch (error) { + if (error.code !== 'ENOENT') { + throw error + } +} + +function saveCanvas() { + console.log('saving canvas') + var data = canvas.toBuffer(undefined, 3, canvas.PNG_FILTER_NONE) + fs.writeFileSync('canvas.png', data) + process.exit() +} +process.on('SIGINT', saveCanvas) +process.on('exit', saveCanvas) + +const server = new wsrpc.Server({ + port: 4242, + service: proto.lookupService('Painter') +}) + +server.implement('paint', async (event) => { + shared.paint(event, ctx) + server.broadcast('paint', PaintEvent.encode(event).finish()) +}) + +server.implement('getCanvas', async (request: CanvasRequest) => { + if (request.width > width || request.height > height) { + throw new Error('Too large') + } + const imageData = ctx.getImageData(0, 0, request.width, request.height) + return new Promise((resolve, reject) => { + const buffer = Buffer.from(imageData.data.buffer) + zlib.gzip(buffer, (error, image) => { + if (error) { reject(error) } else { resolve({image}) } + }) + }) +}) + +const broadcastStatus = () => { + const data = StatusEvent.encode({users: server.connections.length}).finish() + server.broadcast('status', data) +} + +server.on('connection', (connection) => { + broadcastStatus() + connection.once('close', broadcastStatus) +}) + +server.on('error', (error) => { + console.log('error', error.message) +}) + +server.on('listening', () => { + console.log(`listening on ${ server.options.port }`) +}) diff --git a/examples/painter/service.proto b/examples/painter/service.proto deleted file mode 100644 index c3fb3a0..0000000 --- a/examples/painter/service.proto +++ /dev/null @@ -1,18 +0,0 @@ -syntax = "proto3"; - -service Painter { - rpc Paint (PaintMessage) returns (Empty) {} -} - -message Empty {} - -message PaintMessage { - required float x = 1; - required float y = 2; - required float size = 3; - required int32 color = 4; -} - -message StatusMessage { - required int32 users = 1; -} diff --git a/examples/painter/shared/paint.ts b/examples/painter/shared/paint.ts new file mode 100644 index 0000000..3d16485 --- /dev/null +++ b/examples/painter/shared/paint.ts @@ -0,0 +1,62 @@ +import {PaintEvent} from './../protocol/service' +const Canvas = require('canvas') + +const brushSize = 124 +const brushCache: {[color: number]: HTMLCanvasElement} = {} +const brushImage = new Canvas.Image() +brushImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAH0AAAB8CAYAAABE3L+AAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4QjlEQzM5MEMzQTAxMUUwQkU3QUFBOUY2M0QzMUI1MyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4QjlEQzM5MUMzQTAxMUUwQkU3QUFBOUY2M0QzMUI1MyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkRBMjhBRDU3QzM2RjExRTBCRTdBQUE5RjYzRDMxQjUzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkRBMjhBRDU4QzM2RjExRTBCRTdBQUE5RjYzRDMxQjUzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+VTkSEgAAGLFJREFUeNrsXe1y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' + +let brushData: Uint8ClampedArray + +function createCanvas():HTMLCanvasElement { + if (process.title === 'browser') { + return document.createElement('canvas') + } else { + return new Canvas() + } +} + +function getBrush(color: number) { + if (brushCache[color]) { + return brushCache[color] + } + + const r = (color >> 16) & 0xff + const g = (color >> 8) & 0xff + const b = color & 0xff + + const brush = createCanvas() + brush.width = brushSize + brush.height = brushSize + + const ctx = brush.getContext('2d') + + if (!brushData) { + ctx.drawImage(brushImage, 0, 0) + brushData = ctx.getImageData(0, 0, brushSize, brushSize).data + } + + const imageData = ctx.createImageData(brushSize, brushSize) + + for (let i = 0; i < brushData.length; i+=4) { + imageData.data[i] = r + imageData.data[i+1] = g + imageData.data[i+2] = b + imageData.data[i+3] = brushData[i+3] + } + + ctx.putImageData(imageData, 0, 0) + + brushCache[color] = brush + return brush +} + +export function paint(p: PaintEvent, ctx: CanvasRenderingContext2D) { + ctx.globalAlpha = 0.4 + // ctx.globalCompositeOperation = 'overlay' + ctx.fillStyle = '#' + p.color.toString(16) + + const s = Math.min(p.size, 124) + const o = s / 2 + ctx.drawImage(getBrush(p.color), p.x - o, p.y - o, s, s) +}