From 2873157e9798a57b1a46754ce02933af4ac9fe80 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 4 Dec 2014 22:27:07 -0600 Subject: [PATCH] Add 3d render support --- .buildpacks | 2 + app.js | 15 +++-- modules/config.example.js | 8 ++- modules/config.js | 21 ++++++ modules/renders.js | 133 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + routes/renders.js | 109 +++++++++++++++++++++++++++++++ 7 files changed, 279 insertions(+), 10 deletions(-) create mode 100644 .buildpacks create mode 100644 modules/config.js create mode 100644 modules/renders.js create mode 100644 routes/renders.js diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 0000000..516e38b --- /dev/null +++ b/.buildpacks @@ -0,0 +1,2 @@ +https://github.com/mojodna/heroku-buildpack-cairo.git +https://github.com/heroku/heroku-buildpack-nodejs.git \ No newline at end of file diff --git a/app.js b/app.js index ad3b38e..7058de0 100644 --- a/app.js +++ b/app.js @@ -4,9 +4,10 @@ var logger = require("morgan"); var cookieParser = require("cookie-parser"); var bodyParser = require("body-parser"); -var routes = require("./routes/index"); -var avatars = require("./routes/avatars"); -var skins = require("./routes/skins") +var routes = require('./routes/index'); +var avatars = require('./routes/avatars'); +var skins = require('./routes/skins'); +var renders = require('./routes/renders'); var app = express(); @@ -20,10 +21,10 @@ app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); -app.use("/", routes); -app.use("/avatars", avatars); -app.use("/skins", skins) - +app.use('/', routes); +app.use('/avatars', avatars); +app.use('/skins', skins); +app.use('/renders', renders); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/modules/config.example.js b/modules/config.example.js index 3bd43c0..37a8a56 100644 --- a/modules/config.example.js +++ b/modules/config.example.js @@ -8,9 +8,11 @@ var config = { cleaning_limit: 10240, // minumum required available KB on disk to trigger cleaning cleaning_amount: 50000, // amount of avatar (and their helm) files to clean http_timeout: 1000, // ms until connection to mojang is dropped - faces_dir: "skins/faces/", // directory where faces are kept. should have trailing "/" - helms_dir: "skins/helms/", // directory where helms are kept. should have trailing "/" - debug_enabled: false // enables logging.debug + faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' + helms_dir: 'skins/helms/', // directory where helms are kept. should have trailing '/' + debug_enabled: false, // enables logging.debug + default_scale: 6, // the scale of rendered avatars + maximum_sale: 10 // the maximum scale of rendered avatars }; module.exports = config; \ No newline at end of file diff --git a/modules/config.js b/modules/config.js new file mode 100644 index 0000000..037a01d --- /dev/null +++ b/modules/config.js @@ -0,0 +1,21 @@ +var config = { + min_size: 1, // for avatars + max_size: 512, // for avatars; too big values might lead to slow response time or DoS + default_size: 160, // for avatars; size to be used when no size given + local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response + browser_cache_time: 3600, // seconds until browser will request image again + cleaning_interval: 3, // seconds interval: deleting images if disk size at limit + cleaning_limit: 900000000000, // minumum required available KB on disk to trigger cleaning + cleaning_amount: 50000, // amount of avatar (and their helm) files to clean + http_timeout: 1000, // ms until connection to mojang is dropped + faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' + helms_dir: 'skins/helms/', // directory where helms are kept. should have trailing '/' + skins_dir: 'skins/skins/', // directory where skins are kept. should have trailing '/' + renders_dir: 'skins/renders/', // Directory where rendered skins are kept. should have trailing '/' + debug_enabled: true, // enables logging.debug + min_scale: 1, // for renders + max_scale: 100, // for renders; too big values might lead to slow response time or DoS + default_scale: 6, // for renders; scale to be used when no scale given +}; + +module.exports = config; \ No newline at end of file diff --git a/modules/renders.js b/modules/renders.js new file mode 100644 index 0000000..2a54af5 --- /dev/null +++ b/modules/renders.js @@ -0,0 +1,133 @@ +// Skin locations are based on the work of Confuser +// https://github.com/confuser/serverless-mc-skin-viewer +// Permission to use & distribute https://github.com/confuser/serverless-mc-skin-viewer/blob/master/LICENSE + +var helpers = require('./helpers'); + +var exp = {}; + +var Canvas = require('canvas'); +var Image = Canvas.Image; + +exp.draw_helmet = function(skin_canvas, model_ctx, scale) { + //Helmet - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 40*scale, 8*scale, 8*scale, 8*scale, 10*scale, 13/1.2*scale, 8*scale, 8*scale); + //Helmet - Right + model_ctx.setTransform(1,0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 32*scale, 8*scale, 8*scale, 8*scale, 2*scale, 3/1.2*scale, 8*scale, 8*scale); + //Helmet - Top + model_ctx.setTransform(-1,0.5,1,0.5,0,0); + model_ctx.scale(-1,1); + model_ctx.drawImage(skin_canvas, 40*scale, 0, 8*scale, 8*scale, -3*scale, 5*scale, 8*scale, 8*scale); +} + +exp.draw_head = function(skin_canvas, model_ctx, scale) { + //Head - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 8*scale, 8*scale, 8*scale, 8*scale, 10*scale, 13/1.2*scale, 8*scale, 8*scale); + //Head - Right + model_ctx.setTransform(1,0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 0, 8*scale, 8*scale, 8*scale, 2*scale, 3/1.2*scale, 8*scale, 8*scale); + //Head - Top + model_ctx.setTransform(-1,0.5,1,0.5,0,0); + model_ctx.scale(-1,1); + model_ctx.drawImage(skin_canvas, 8*scale, 0, 8*scale, 8*scale, -3*scale, 5*scale, 8*scale, 8*scale); +} + +exp.draw_body = function(skin_canvas, model_ctx, scale) { + //Left Leg + //Left Leg - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.scale(-1,1); + model_ctx.drawImage(skin_canvas, 4*scale, 20*scale, 4*scale, 12*scale, -16*scale, 34.4/1.2*scale, 4*scale, 12*scale); + + //Right Leg + //Right Leg - Right + model_ctx.setTransform(1,0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 0*scale, 20*scale, 4*scale, 12*scale, 4*scale, 26.4/1.2*scale, 4*scale, 12*scale); + //Right Leg - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 4*scale, 20*scale, 4*scale, 12*scale, 8*scale, 34.4/1.2*scale, 4*scale, 12*scale); + + //Arm Left + //Arm Left - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.scale(-1,1); + model_ctx.drawImage(skin_canvas, 44*scale, 20*scale, 4*scale, 12*scale, -20*scale, 20/1.2*scale, 4*scale, 12*scale); + //Arm Left - Top + model_ctx.setTransform(-1,0.5,1,0.5,0,0); + model_ctx.drawImage(skin_canvas, 44*scale, 16*scale, 4*scale, 4*scale, 0, 16*scale, 4*scale, 4*scale); + + //Body + //Body - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 20*scale, 20*scale, 8*scale, 12*scale, 8*scale, 20/1.2*scale, 8*scale, 12*scale); + + //Arm Right + //Arm Right - Right + model_ctx.setTransform(1,0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 40*scale, 20*scale, 4*scale, 12*scale, 0, 16/1.2*scale, 4*scale, 12*scale); + //Arm Right - Front + model_ctx.setTransform(1,-0.5,0,1.2,0,0); + model_ctx.drawImage(skin_canvas, 44*scale, 20*scale, 4*scale, 12*scale, 4*scale, 20/1.2*scale, 4*scale, 12*scale); + //Arm Right - Top + model_ctx.setTransform(-1,0.5,1,0.5,0,0); + model_ctx.scale(-1,1); + model_ctx.drawImage(skin_canvas, 44*scale, 16*scale, 4*scale, 4*scale, -16*scale, 16*scale, 4*scale, 4*scale); +} + +exp.draw_model = function(uuid, scale, helm, body, callback) { + helpers.get_skin(uuid, function(err, hash, img) { + var image = new Image; + var width = 64 * scale; + var height = 64 * scale; + var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale); + var skin_canvas = new Canvas(width, height); + var model_ctx = model_canvas.getContext('2d'); + var skin_ctx = skin_canvas.getContext('2d'); + + image.onerror = function(err) { + console.log("render error: " + err); + callback(err, 2, null, hash); + }; + + image.onload = function() { + skin_ctx.drawImage(image,0,0,64,64,0,0,64,64); + //Scale it + scale_image(skin_ctx.getImageData(0,0,64,64), skin_ctx, 0, 0, scale); + if (body) { + console.log("drawing body"); + exp.draw_body(skin_canvas, model_ctx, scale); + } + console.log("drawing head"); + exp.draw_head(skin_canvas, model_ctx, scale); + if (helm) { + console.log("drawing helmet"); + exp.draw_helmet(skin_canvas, model_ctx, scale); + } + + model_canvas.toBuffer(function(err, buf){ + callback(err, 2, buf, hash); + }); + }; + + image.src = img; + }); +} + +function scale_image(imageData, context, d_x, d_y, scale) { + var width = imageData.width; + var height = imageData.height; + context.clearRect(0,0,width,height); //Clear the spot where it originated from + for(y=0; y config.maximum_scale) { + // Preventing from OOM crashes. + res.status(422).send("422 Invalid Size"); + } else if (!helpers.uuid_valid(uuid)) { + res.status(422).send("422 Invalid UUID"); + return; + } + + // strip dashes + uuid = uuid.replace(/-/g, ""); + + try { + renders.draw_model(uuid, scale, helm, body, function(err, status, image, hash) { + logging.log(uuid + " - " + human_status[status]); + if (err) { + logging.error(err); + } + etag = hash && hash.substr(0, 32) || "none"; + var matches = req.get("If-None-Match") == '"' + etag + '"'; + if (image) { + var http_status = 200; + if (matches) { + http_status = 304; + } else if (err) { + http_status = 503; + } + logging.log("matches: " + matches); + logging.log("Etag: " + req.get("If-None-Match")); + logging.log("status: " + http_status); + sendimage(http_status, status, image); + } else { + //handle_default(404, status); + } + }); + } catch(e) { + logging.error("Error!"); + logging.error(e); + //handle_default(500, status); + } + + function handle_default(http_status, img_status) { + if (def && def != "steve" && def != "alex") { + res.writeHead(301, { + 'Cache-Control': 'max-age=' + config.browser_cache_time + ', public', + 'Response-Time': new Date() - start, + 'X-Storage-Type': human_status[img_status], + 'Access-Control-Allow-Origin': '*', + 'Location': def + }); + res.end(); + } else { + def = def || skins.default_skin(uuid); + skins.resize_img("public/images/" + def + ".png", size, function(err, image) { + sendimage(http_status, img_status, image); + }); + } + } + + function sendimage(http_status, img_status, image) { + res.writeHead(http_status, { + 'Content-Type': 'image/png', + 'Cache-Control': 'max-age=' + config.browser_cache_time + ', public', + 'Response-Time': new Date() - start, + 'X-Storage-Type': human_status[img_status], + 'Access-Control-Allow-Origin': '*', + 'Etag': '"' + etag + '"' + }); + res.end(http_status == 304 ? null : image); + } +}); + +module.exports = router; \ No newline at end of file