@ -0,0 +1,2 @@ |
|||||
|
node_modules |
||||
|
.env |
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"bitwise": true, |
||||
|
"browser": true, |
||||
|
"curly": true, |
||||
|
"eqeqeq": true, |
||||
|
"latedef": true, |
||||
|
"noarg": true, |
||||
|
"node": true, |
||||
|
"strict": "global", |
||||
|
"undef": true, |
||||
|
"unused": true, |
||||
|
"futurehostile": true, |
||||
|
"esversion": 9, |
||||
|
"mocha": true, |
||||
|
"globals": { |
||||
|
"markdown": true, |
||||
|
"moment": true, |
||||
|
"numeral": true, |
||||
|
"io": true, |
||||
|
"Chart": true, |
||||
|
"CodeMirror": true, |
||||
|
"UIkit": true, |
||||
|
"twttr": true |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
# Multiplayer Canvas |
||||
|
|
||||
|
An HTML5 <canvas> to which multiple people can connect over Socket.io and make changes one pixel at a time. |
||||
|
|
||||
|
## Running the Application |
||||
|
|
||||
|
```sh |
||||
|
yarn start |
||||
|
``` |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 215 B |
After Width: | Height: | Size: 252 B |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,26 @@ |
|||||
|
doctype html |
||||
|
html(lang="en") |
||||
|
|
||||
|
head |
||||
|
|
||||
|
meta(charset="UTF-8") |
||||
|
meta(http-equiv="X-UA-Compatible", content="IE=edge") |
||||
|
meta(name="viewport", content="width=device-width, initial-scale=1.0") |
||||
|
|
||||
|
title Multiplayer Canvas |
||||
|
|
||||
|
link(rel="stylesheet", href="/dist/canvas-app.css") |
||||
|
link(rel="icon", type="image/x-icon", href="/dist/assets/icon/favicon.ico") |
||||
|
|
||||
|
body(data-dtp-env= process.env.NODE_ENV) |
||||
|
|
||||
|
.navbar |
||||
|
.brand-link |
||||
|
a(href="https://nicecrew.digital", target="_blank") |
||||
|
img(src="/dist/assets/img/nicecrew-banner.png", alt="NiceCrew Banner").brand-header |
||||
|
|
||||
|
.container |
||||
|
|
||||
|
block content-view |
||||
|
|
||||
|
script(src=`${appModuleUrl}?v=${pkg.version}`) |
@ -0,0 +1,4 @@ |
|||||
|
extends layout |
||||
|
block content-view |
||||
|
.margin |
||||
|
canvas(id="multiplayer-canvas", width="32", height="32").multiplayer-canvas |
@ -0,0 +1,50 @@ |
|||||
|
// canvas-app.js
|
||||
|
// Copyright (C) 2022 Rob Colbert @[email protected]
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
console.log('loaded'); |
||||
|
|
||||
|
const IMAGE_W = 32; |
||||
|
const IMAGE_H = 32; |
||||
|
|
||||
|
const DTP_COMPONENT_NAME = 'canvas-app'; |
||||
|
import '../less/style.less'; |
||||
|
|
||||
|
import { NiceAudio, NiceLog } from 'dtp-nice-game'; |
||||
|
|
||||
|
export default class CanvasApp { |
||||
|
|
||||
|
constructor ( ) { |
||||
|
this.audio = new NiceAudio(); |
||||
|
|
||||
|
this.log = new NiceLog(DTP_COMPONENT_NAME); |
||||
|
this.log.info('Canvas app online'); |
||||
|
|
||||
|
this.canvas = document.querySelector('canvas'); |
||||
|
this.ctx = this.canvas.getContext('2d'); |
||||
|
this.imageData = this.ctx.createImageData(32, 32); |
||||
|
} |
||||
|
|
||||
|
updatePixels (message) { |
||||
|
for (const pixel of message.pixels) { |
||||
|
if ((pixel.x < 0) || |
||||
|
(pixel.x > (IMAGE_W - 1)) || |
||||
|
(pixel.y < 0) || |
||||
|
(pixel.y > (IMAGE_H - 1))) { |
||||
|
this.log.info('rejecting invalid pixel update', pixel); |
||||
|
return; |
||||
|
} |
||||
|
let pixelIdx = (pixel.y * IMAGE_W * 4) + (pixel.x * 4); |
||||
|
this.imageData[pixelIdx++] = pixel.r; |
||||
|
this.imageData[pixelIdx++] = pixel.g; |
||||
|
this.imageData[pixelIdx++] = pixel.b; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
window.addEventListener('load', async ( ) => { |
||||
|
window.app = new CanvasApp(); |
||||
|
console.log('running'); |
||||
|
}); |
@ -0,0 +1,23 @@ |
|||||
|
/* |
||||
|
* lib/brand-link.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
.brand-link { |
||||
|
display: block; |
||||
|
width: 240px; |
||||
|
margin: 0 auto; |
||||
|
|
||||
|
img.brand-header { |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
max-width: 960px; |
||||
|
height: auto; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.brand-prompt { |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
/* |
||||
|
* lib/container.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
.container { |
||||
|
width: 100%; |
||||
|
max-width: 1280px; |
||||
|
margin: 0 auto; |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
/* |
||||
|
* lib/main.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
html, body { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
font-size: 14px; |
||||
|
|
||||
|
background-color: var(--background-color); |
||||
|
color: var(--color); |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.margin { |
||||
|
display: block; |
||||
|
margin: var(--default-margin) 0; |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
/* |
||||
|
* lib/multiplayer-canvas.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
canvas.multiplayer-canvas { |
||||
|
display: block; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
width: 100%; |
||||
|
max-width: 640px; |
||||
|
height: auto; |
||||
|
|
||||
|
border: solid 1px white; |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
/* |
||||
|
* lib/navbar.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
.navbar { |
||||
|
padding: var(--default-margin) 0; |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
/* |
||||
|
* lib/variables.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
* { |
||||
|
--background-color : #1a1a1a; |
||||
|
--color : #e8e8e8; |
||||
|
--brand-color : rgb(4, 130, 216); |
||||
|
|
||||
|
--default-margin : 30px; |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
/* |
||||
|
* style.less |
||||
|
* Copyright (C) 2022 Rob Colbert @[email protected] |
||||
|
* License: Apache-2.0 |
||||
|
*/ |
||||
|
|
||||
|
@import 'lib/variables.less'; |
||||
|
@import 'lib/main.less'; |
||||
|
|
||||
|
@import 'lib/brand-link.less'; |
||||
|
@import 'lib/container.less'; |
||||
|
@import 'lib/multiplayer-canvas.less'; |
||||
|
@import 'lib/navbar.less'; |
@ -0,0 +1,104 @@ |
|||||
|
// multiplayer-canvas.js
|
||||
|
// Copyright (C) 2022 Rob Colbert @[email protected]
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import 'dotenv/config'; // reads .env into process.env
|
||||
|
|
||||
|
import path, { dirname } from 'path'; |
||||
|
import { fileURLToPath } from 'url'; |
||||
|
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
||||
|
|
||||
|
import { createRequire } from 'module'; |
||||
|
const require = createRequire(import.meta.url); // jshint ignore:line
|
||||
|
|
||||
|
import express from 'express'; |
||||
|
|
||||
|
import winston from 'winston'; |
||||
|
import expressWinston from 'express-winston'; |
||||
|
|
||||
|
import webpack from 'webpack'; |
||||
|
import webpackDevMiddleware from 'webpack-dev-middleware'; |
||||
|
|
||||
|
import WEBPACK_CONFIG from './webpack.config.js'; |
||||
|
|
||||
|
const APP_CONFIG = { |
||||
|
pkg: require('./package.json'), |
||||
|
niceGameSdk: require('./node_modules/dtp-nice-game/package.json'), |
||||
|
}; |
||||
|
|
||||
|
class MultiplayerCanvasApp { |
||||
|
|
||||
|
constructor ( ) { |
||||
|
this.app = express(); |
||||
|
|
||||
|
this.app.locals.config = APP_CONFIG; |
||||
|
this.app.locals.pkg = APP_CONFIG.pkg; |
||||
|
this.app.locals.niceGameSdk = APP_CONFIG.niceGameSdk; |
||||
|
|
||||
|
this.app.set('view engine', 'pug'); |
||||
|
this.app.set('views', path.join(__dirname, 'app', 'views')); |
||||
|
|
||||
|
this.app.use(expressWinston.logger({ |
||||
|
transports: [ |
||||
|
new winston.transports.Console(), |
||||
|
], |
||||
|
format: winston.format.combine( |
||||
|
winston.format.colorize(), |
||||
|
// winston.format.json(),
|
||||
|
), |
||||
|
meta: false, |
||||
|
msg: "HTTP ", |
||||
|
expressFormat: true, |
||||
|
colorize: false, |
||||
|
ignoreRoute: (/*req, res*/) => { return false; }, |
||||
|
})); |
||||
|
|
||||
|
this.app.use('/dist/assets', express.static(path.join(__dirname, 'app', 'assets'))); |
||||
|
|
||||
|
/* |
||||
|
* Webpack integration |
||||
|
*/ |
||||
|
|
||||
|
this.compiler = webpack(WEBPACK_CONFIG); |
||||
|
|
||||
|
/* |
||||
|
* Webpack dev server middleware |
||||
|
*/ |
||||
|
|
||||
|
this.webpackDevMiddleware = webpackDevMiddleware(this.compiler, { |
||||
|
publicPath: WEBPACK_CONFIG.output.publicPath, |
||||
|
}); |
||||
|
this.app.use(this.webpackDevMiddleware); |
||||
|
|
||||
|
/* |
||||
|
* Application routes |
||||
|
*/ |
||||
|
|
||||
|
this.app.get('/', this.getCanvasView.bind(this)); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
this.app.listen(3000, (err) => { |
||||
|
if (err) { |
||||
|
return reject(err); |
||||
|
} |
||||
|
resolve(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async getCanvasView (req, res) { |
||||
|
res.locals.appModuleUrl = '/dist/canvas-app.bundle.js'; |
||||
|
res.render('multiplayer-canvas'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
(async ( ) => { |
||||
|
|
||||
|
const app = new MultiplayerCanvasApp(); |
||||
|
await app.start(); |
||||
|
|
||||
|
})(); |
@ -0,0 +1,102 @@ |
|||||
|
// webpack.config.js
|
||||
|
// Copyright (C) 2022 Rob Colbert @[email protected]
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import path, { dirname } from 'path'; |
||||
|
import { fileURLToPath } from 'url'; |
||||
|
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
||||
|
|
||||
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; |
||||
|
import BrowserSyncPlugin from 'browser-sync-webpack-plugin'; |
||||
|
|
||||
|
const webpackMode = (process.env.NODE_ENV === 'production') ? 'production' : 'development'; |
||||
|
console.log('Webpack mode:', webpackMode); |
||||
|
|
||||
|
const plugins = [ ]; |
||||
|
|
||||
|
plugins.push(new MiniCssExtractPlugin()); |
||||
|
|
||||
|
if (webpackMode === 'development') { |
||||
|
plugins.push( |
||||
|
new BrowserSyncPlugin( |
||||
|
{ |
||||
|
proxy: { |
||||
|
target: 'http://localhost:3000', |
||||
|
ws: true, |
||||
|
}, |
||||
|
host: 'localhost', |
||||
|
open: 'local', |
||||
|
port: 3333, |
||||
|
cors: true, |
||||
|
ui: { |
||||
|
port: 3400, |
||||
|
}, |
||||
|
notify: false, |
||||
|
ghostMode: { |
||||
|
clicks: false, |
||||
|
forms: false, |
||||
|
scroll: true, |
||||
|
}, |
||||
|
logLevel: 'info', |
||||
|
files: [ |
||||
|
'./dist/*.js', |
||||
|
'./dist/*.css', |
||||
|
], |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
entry: { |
||||
|
'canvas-app': './client/js/canvas-app.js', |
||||
|
}, |
||||
|
mode: webpackMode, |
||||
|
output: { |
||||
|
filename: '[name].bundle.js', |
||||
|
path: path.resolve(__dirname, 'dist'), |
||||
|
clean: true, |
||||
|
publicPath: '/dist', |
||||
|
}, |
||||
|
optimization: { |
||||
|
splitChunks: { |
||||
|
chunks: 'all', |
||||
|
}, |
||||
|
}, |
||||
|
plugins, |
||||
|
module: { |
||||
|
rules: [ |
||||
|
{ |
||||
|
test: /\.less$/i, |
||||
|
use: [ |
||||
|
{ |
||||
|
loader: "style-loader", |
||||
|
}, |
||||
|
{ |
||||
|
loader: MiniCssExtractPlugin.loader, |
||||
|
options: { |
||||
|
esModule: false, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
loader: "css-loader", |
||||
|
options: { |
||||
|
sourceMap: true, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
loader: "less-loader", |
||||
|
options: { |
||||
|
sourceMap: true, |
||||
|
lessOptions: { |
||||
|
strictMath: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
}; |