@ -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 @rob@nicecrew.digital
|
|||
// 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 @rob@nicecrew.digital |
|||
* 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 @rob@nicecrew.digital |
|||
* 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 @rob@nicecrew.digital |
|||
* 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 @rob@nicecrew.digital |
|||
* 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 @rob@nicecrew.digital |
|||
* License: Apache-2.0 |
|||
*/ |
|||
|
|||
.navbar { |
|||
padding: var(--default-margin) 0; |
|||
} |
@ -0,0 +1,13 @@ |
|||
/* |
|||
* lib/variables.less |
|||
* Copyright (C) 2022 Rob Colbert @rob@nicecrew.digital |
|||
* 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 @rob@nicecrew.digital |
|||
* 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 @rob@nicecrew.digital
|
|||
// 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 @rob@nicecrew.digital
|
|||
// 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, |
|||
}, |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
}; |