1236 lines
38 KiB
JavaScript
1236 lines
38 KiB
JavaScript
var fs = require('fs');
|
|
var path = require('path');
|
|
var Canvas = require('canvas');
|
|
var Image = Canvas.Image;
|
|
|
|
/**
|
|
* Sunwell
|
|
* =======
|
|
* Sunwell is a renderer for hearthstone cards.
|
|
*
|
|
* @author Christian Engel <hello@wearekiss.com>
|
|
* @author Matthias Klein <matthias.a.klein@gmail.com> (node.js port)
|
|
*/
|
|
|
|
module.exports = function(settings) {
|
|
'use strict';
|
|
|
|
var sunwell = {},
|
|
pluralRegex = /(\d+)(.+?)\|4\((.+?),(.+?)\)/g,
|
|
validRarity = ['COMMON', 'RARE', 'EPIC', 'LEGENDARY'];
|
|
|
|
function log(msg) {
|
|
if (!sunwell.settings.debug) {
|
|
return;
|
|
}
|
|
console.log(msg);
|
|
}
|
|
|
|
/**
|
|
* Returns a new render buffer (canvas).
|
|
* @returns {*}
|
|
*/
|
|
function getBuffer() {
|
|
return new Canvas();
|
|
}
|
|
|
|
var imgReplacement;
|
|
|
|
function getMissingImg(assetId) {
|
|
log('Substitute for ' + assetId);
|
|
|
|
if (imgReplacement) {
|
|
return imgReplacement;
|
|
}
|
|
|
|
var buffer = getBuffer(),
|
|
bufferctx = buffer.getContext('2d');
|
|
buffer.width = buffer.height = 512;
|
|
bufferctx.save();
|
|
bufferctx.fillStyle = 'grey';
|
|
bufferctx.fillRect(0, 0, 512, 512);
|
|
bufferctx.fill();
|
|
bufferctx.fillStyle = 'red';
|
|
bufferctx.textAlign = 'center';
|
|
bufferctx.textBaseline = 'middle';
|
|
bufferctx.font = '50px Belwe';
|
|
bufferctx.fillText('missing artwork', 256, 256);
|
|
bufferctx.restore();
|
|
imgReplacement = new Image();
|
|
imgReplacement.src = buffer.toBuffer();
|
|
return imgReplacement;
|
|
}
|
|
|
|
sunwell.settings = settings || {};
|
|
|
|
sunwell.settings.titleFont = sunwell.settings.titleFont || 'Belwe Bold';
|
|
sunwell.settings.bodyFont = sunwell.settings.bodyFont || 'ITC Franklin Condensed';
|
|
sunwell.settings.bodyFontSize = sunwell.settings.bodyFontSize || 60;
|
|
sunwell.settings.bodyFontOffset = sunwell.settings.bodyFontOffset || {x: 0, y: 0};
|
|
sunwell.settings.bodyLineHeight = sunwell.settings.bodyLineHeight || 50;
|
|
sunwell.settings.assetFolder = sunwell.settings.assetFolder || path.join(__dirname, 'assets');
|
|
sunwell.settings.assetExtension = sunwell.settings.assetExtension || 'png';
|
|
sunwell.settings.textureFolder = sunwell.settings.textureFolder || path.join(__dirname, 'artwork');
|
|
sunwell.settings.textureExtension = sunwell.settings.textureExtension || 'jpg';
|
|
sunwell.settings.smallTextureFolder = sunwell.settings.smallTextureFolder || null;
|
|
sunwell.settings.smallTextureExtension = sunwell.settings.smallTextureExtension || 'jpg';
|
|
sunwell.settings.autoInit = sunwell.settings.autoInit || true;
|
|
sunwell.settings.idAsTexture = sunwell.settings.idAsTexture || false;
|
|
|
|
|
|
sunwell.settings.debug = sunwell.settings.debug || false;
|
|
|
|
sunwell.races = {
|
|
'enUS': {
|
|
'MURLOC': 'Murloc',
|
|
'MECHANICAL': 'Mech',
|
|
'BEAST': 'Beast',
|
|
'DEMON': 'Demon',
|
|
'PIRATE': 'Pirate',
|
|
'DRAGON': 'Dragon',
|
|
'TOTEM': 'Totem',
|
|
'HERO': 'Hero'
|
|
},
|
|
"deDE": {
|
|
'MURLOC': 'Murloc',
|
|
'MECHANICAL': 'Mech',
|
|
'BEAST': 'Wildtier',
|
|
'DEMON': 'Dämon',
|
|
'PIRATE': 'Pirat',
|
|
'DRAGON': 'Drache',
|
|
'TOTEM': 'Totem'
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Calculate the checksum for an object.
|
|
* @param o
|
|
* @returns {number}
|
|
*/
|
|
function checksum(o) {
|
|
var i, key, s = '';
|
|
var chk = 0x12345678;
|
|
|
|
for (key in o) {
|
|
s = s + key + o[key];
|
|
}
|
|
|
|
for (i = 0; i < s.length; i++) {
|
|
chk += (s.charCodeAt(i) * (i + 1));
|
|
}
|
|
|
|
return chk;
|
|
}
|
|
|
|
/**
|
|
* Helper function to draw the oval mask for the cards artwork.
|
|
* @param ctx
|
|
* @param x
|
|
* @param y
|
|
* @param w
|
|
* @param h
|
|
*/
|
|
function drawEllipse(ctx, x, y, w, h) {
|
|
var kappa = .5522848,
|
|
ox = (w / 2) * kappa, // control point offset horizontal
|
|
oy = (h / 2) * kappa, // control point offset vertical
|
|
xe = x + w, // x-end
|
|
ye = y + h, // y-end
|
|
xm = x + w / 2, // x-middle
|
|
ym = y + h / 2; // y-middle
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, ym);
|
|
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
|
|
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
|
|
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
|
|
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
|
|
//ctx.closePath(); // not used correctly, see comments (use to close off open path)
|
|
ctx.stroke();
|
|
}
|
|
|
|
/**
|
|
* Preloads the basic card assets.
|
|
* You can call this before you start to render any cards, but you don't have to.
|
|
*/
|
|
function fetchAssets(loadAssets) {
|
|
return new Promise(function (resolve) {
|
|
var loaded = 0,
|
|
loadingTotal = 1,
|
|
assets = {},
|
|
key,
|
|
isTexture,
|
|
smallTexture,
|
|
isUrl,
|
|
srcURL;
|
|
|
|
for (var i = 0; i < loadAssets.length; i++) {
|
|
key = loadAssets[i];
|
|
isTexture = false;
|
|
smallTexture = false;
|
|
|
|
if (key.substr(0, 2) === 'h:') {
|
|
isTexture = true;
|
|
smallTexture = !!(sunwell.settings.smallTextureFolder && true);
|
|
key = key.substr(2);
|
|
}
|
|
|
|
if (key.substr(0, 2) === 't:') {
|
|
isTexture = true;
|
|
key = key.substr(2);
|
|
if (assets[key] !== undefined) {
|
|
if (assets[key].width === 256) {
|
|
assets[key] = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (key.substr(0, 2) === 'u:') {
|
|
isUrl = true;
|
|
key = key.substr(2);
|
|
}
|
|
|
|
assets[key] = new Image();
|
|
assets[key].crossOrigin = "Anonymous";
|
|
assets[key].loaded = false;
|
|
loadingTotal++;
|
|
(function (key) {
|
|
assets[key].onload = function () {
|
|
loaded++;
|
|
assets[key].loaded = true;
|
|
if (!assets[key].width || !assets[key].height) {
|
|
assets[key] = getMissingImg();
|
|
}
|
|
if (loaded >= loadingTotal) {
|
|
resolve(assets);
|
|
}
|
|
};
|
|
assets[key].onerror = function () {
|
|
loaded++;
|
|
|
|
assets[key] = getMissingImg();
|
|
if (loaded >= loadingTotal) {
|
|
resolve(assets);
|
|
}
|
|
};
|
|
})(key);
|
|
if (isUrl) {
|
|
assets[key].src = key;
|
|
} else {
|
|
if (isTexture) {
|
|
assets[key].isTexture = true;
|
|
if (smallTexture) {
|
|
srcURL = path.join(sunwell.settings.smallTextureFolder, key + '.' + sunwell.settings.smallTextureExtension);
|
|
} else {
|
|
srcURL = path.join(sunwell.settings.textureFolder, key + '.' + sunwell.settings.textureExtension);
|
|
}
|
|
} else {
|
|
srcURL = path.join(sunwell.settings.assetFolder, key + '.' + sunwell.settings.assetExtension);
|
|
}
|
|
log('Requesting ' + srcURL);
|
|
assets[key].src = srcURL;
|
|
}
|
|
}
|
|
|
|
loadingTotal--;
|
|
if (loaded > 0 && loaded >= loadingTotal) {
|
|
resolve(assets);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the bounding box of a canvas' content.
|
|
* @param ctx
|
|
* @param alphaThreshold
|
|
* @returns {{x: *, y: *, maxX: (number|*|w), maxY: *, w: number, h: number}}
|
|
*/
|
|
function contextBoundingBox(ctx) {
|
|
var w = ctx.canvas.width, h = ctx.canvas.height;
|
|
var data = ctx.getImageData(0, 0, w, h).data;
|
|
var x, y, minX = 999, minY = 999, maxX = 0, maxY = 0;
|
|
|
|
var out = false;
|
|
|
|
for (y = h - 1; y > -1; y--) {
|
|
if (out) {
|
|
break;
|
|
}
|
|
for (x = 0; x < w; x++) {
|
|
if (data[((y * (w * 4)) + (x * 4)) + 3] > 0) {
|
|
maxY = Math.max(maxY, y);
|
|
out = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (maxY === undefined) {
|
|
return null;
|
|
}
|
|
|
|
out2:
|
|
for (x = w - 1; x > -1; x--) {
|
|
for (y = 0; y < h; y++) {
|
|
if (data[((y * (w * 4)) + (x * 4)) + 3] > 0) {
|
|
maxX = Math.max(maxX, x);
|
|
break out2;
|
|
}
|
|
}
|
|
}
|
|
|
|
out3:
|
|
for (x = 0; x < maxX; x++) {
|
|
for (y = 0; y < h; y++) {
|
|
if (data[((y * (w * 4)) + (x * 4)) + 3] > 0) {
|
|
minX = Math.min(x, minX);
|
|
break out3;
|
|
}
|
|
}
|
|
}
|
|
|
|
out4:
|
|
for (y = 0; y < maxY; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
if (data[((y * (w * 4)) + (x * 4)) + 3] > 0) {
|
|
minY = Math.min(minY, y);
|
|
break out4;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {x: minX, y: minY, maxX: maxX, maxY: maxY, w: maxX - minX, h: maxY - minY};
|
|
}
|
|
|
|
function renderRaceText(targetCtx, s, card) {
|
|
var text, x;
|
|
|
|
if (sunwell.races[card.language]) {
|
|
if (sunwell.races[card.language][card.race]) {
|
|
text = sunwell.races[card.language][card.race];
|
|
} else {
|
|
text = card.race;
|
|
}
|
|
} else {
|
|
if (sunwell.races['enUS'][card.race]) {
|
|
text = sunwell.races['enUS'][card.race];
|
|
} else {
|
|
text = card.race;
|
|
}
|
|
}
|
|
|
|
var buffer = getBuffer();
|
|
var bufferCtx = buffer.getContext('2d');
|
|
|
|
buffer.width = 300;
|
|
buffer.height = 60;
|
|
|
|
bufferCtx.font = '45px Belwe';
|
|
|
|
bufferCtx.lineCap = 'round';
|
|
bufferCtx.lineJoin = 'round';
|
|
bufferCtx.textBaseline = 'hanging';
|
|
|
|
bufferCtx.textAlign = 'left';
|
|
|
|
text = text.split('');
|
|
|
|
x = 10;
|
|
|
|
for (var i = 0; i < text.length; i++) {
|
|
bufferCtx.lineWidth = 8;
|
|
bufferCtx.strokeStyle = 'black';
|
|
bufferCtx.fillStyle = 'black';
|
|
bufferCtx.fillText(text[i], x, 10);
|
|
bufferCtx.strokeText(text[i], x, 10);
|
|
|
|
bufferCtx.fillStyle = 'white';
|
|
bufferCtx.strokeStyle = 'white';
|
|
bufferCtx.lineWidth = 1;
|
|
bufferCtx.fillText(text[i], x, 10);
|
|
//ctx.strokeText(text[i], x, y);
|
|
|
|
x += bufferCtx.measureText(text[i]).width;
|
|
}
|
|
|
|
var b = contextBoundingBox(bufferCtx);
|
|
|
|
targetCtx.drawImage(buffer, b.x, b.y, b.w, b.h, (394 - (b.w / 2)) * s, (1001 - (b.h / 2)) * s, b.w * s, b.h * s);
|
|
}
|
|
|
|
/**
|
|
* Renders a given number to the defined position.
|
|
* The x/y position should be the position on an unscaled card.
|
|
*
|
|
* @param targetCtx
|
|
* @param x
|
|
* @param y
|
|
* @param s
|
|
* @param number
|
|
* @param size
|
|
* @param [drawStyle="0"] Either "+", "-" or "0". Default: "0"
|
|
*/
|
|
function drawNumber(targetCtx, x, y, s, number, size, drawStyle) {
|
|
var buffer = getBuffer();
|
|
var bufferCtx = buffer.getContext('2d');
|
|
|
|
if (drawStyle === undefined) {
|
|
drawStyle = '0';
|
|
}
|
|
|
|
buffer.width = 256;
|
|
buffer.height = 256;
|
|
|
|
number = number.toString();
|
|
|
|
number = number.split('');
|
|
|
|
var tX = 10;
|
|
|
|
bufferCtx.font = size + 'px Belwe';
|
|
|
|
bufferCtx.lineCap = 'round';
|
|
bufferCtx.lineJoin = 'round';
|
|
bufferCtx.textBaseline = 'hanging';
|
|
|
|
bufferCtx.textAlign = 'left';
|
|
|
|
var color = 'white';
|
|
|
|
if (drawStyle === '-') {
|
|
color = '#f00';
|
|
}
|
|
|
|
if (drawStyle === '+') {
|
|
color = '#0f0';
|
|
}
|
|
|
|
for (var i = 0; i < number.length; i++) {
|
|
bufferCtx.lineWidth = 13;
|
|
bufferCtx.strokeStyle = 'black';
|
|
bufferCtx.fillStyle = 'black';
|
|
bufferCtx.fillText(number[i], tX, 10);
|
|
bufferCtx.strokeText(number[i], tX, 10);
|
|
|
|
bufferCtx.fillStyle = color;
|
|
bufferCtx.strokeStyle = color;
|
|
bufferCtx.lineWidth = 2.5;
|
|
bufferCtx.fillText(number[i], tX, 10);
|
|
//ctx.strokeText(text[i], x, y);
|
|
|
|
tX += bufferCtx.measureText(number[i]).width;
|
|
}
|
|
|
|
var b = contextBoundingBox(bufferCtx);
|
|
|
|
targetCtx.drawImage(buffer, b.x, b.y, b.w, b.h, (x - (b.w / 2)) * s, (y - (b.h / 2)) * s, b.w * s, b.h * s);
|
|
}
|
|
|
|
/**
|
|
* Finishes a text line and starts a new one.
|
|
* @param bufferTextCtx
|
|
* @param bufferRow
|
|
* @param bufferRowCtx
|
|
* @param xPos
|
|
* @param yPos
|
|
* @param totalWidth
|
|
* @returns {*[]}
|
|
*/
|
|
function finishLine(bufferTextCtx, bufferRow, bufferRowCtx, xPos, yPos, totalWidth) {
|
|
if (sunwell.settings.debug) {
|
|
bufferTextCtx.save();
|
|
bufferTextCtx.strokeStyle = 'red';
|
|
bufferTextCtx.beginPath();
|
|
bufferTextCtx.moveTo((totalWidth / 2) - (xPos / 2), yPos);
|
|
bufferTextCtx.lineTo((totalWidth / 2) + (xPos / 2), yPos);
|
|
bufferTextCtx.stroke();
|
|
bufferTextCtx.restore();
|
|
}
|
|
|
|
var xCalc = (totalWidth / 2) - (xPos / 2);
|
|
|
|
if (xCalc < 0) {
|
|
xCalc = 0;
|
|
}
|
|
|
|
if (xPos > 0 && bufferRow.width > 0) {
|
|
bufferTextCtx.drawImage(
|
|
bufferRow,
|
|
0,
|
|
0,
|
|
xPos > bufferRow.width ? bufferRow.width : xPos,
|
|
bufferRow.height,
|
|
xCalc,
|
|
yPos,
|
|
Math.min(xPos, bufferRow.width),
|
|
bufferRow.height
|
|
);
|
|
}
|
|
|
|
xPos = 5;
|
|
yPos += bufferRow.height;
|
|
bufferRowCtx.clearRect(0, 0, bufferRow.width, bufferRow.height);
|
|
|
|
return [xPos, yPos];
|
|
}
|
|
|
|
/**
|
|
* Renders the HTML body text of a card.
|
|
* @param targetCtx
|
|
* @param s
|
|
* @param card
|
|
*/
|
|
function drawBodyText(targetCtx, s, card, forceSmallerFirstLine) {
|
|
var manualBreak = card.text.substr(0, 3) === '[x]',
|
|
bufferText = getBuffer(),
|
|
bufferTextCtx = bufferText.getContext('2d'),
|
|
bufferRow = getBuffer(),
|
|
bufferRowCtx = bufferRow.getContext('2d'),
|
|
bodyText = manualBreak ? card.text.substr(3) : card.text,
|
|
words,
|
|
word,
|
|
chars,
|
|
char,
|
|
width,
|
|
spaceWidth,
|
|
xPos = 0,
|
|
yPos = 0,
|
|
isBold = 0,
|
|
isItalic = 0,
|
|
i,
|
|
j,
|
|
r,
|
|
centerLeft,
|
|
centerTop,
|
|
justLineBreak,
|
|
lineCount = 0,
|
|
plurals,
|
|
pBodyText;
|
|
|
|
|
|
pBodyText = bodyText;
|
|
while((plurals = pluralRegex.exec(bodyText)) !== null){
|
|
pBodyText = pBodyText.replace(plurals[0], plurals[1] + plurals[2] + (parseInt(plurals[1], 10) === 1 ? plurals[3] : plurals[4]));
|
|
}
|
|
bodyText = pBodyText;
|
|
|
|
words = bodyText.replace(/[\$#_]/g, '').split(/( |\n)/g);
|
|
|
|
log('Rendering body: ' + bodyText);
|
|
|
|
centerLeft = 390;
|
|
centerTop = 860;
|
|
bufferText.width = 520;
|
|
bufferText.height = 290;
|
|
|
|
bufferRow.width = 520;
|
|
|
|
if (card.type === 'SPELL') {
|
|
bufferText.width = 460;
|
|
bufferText.height = 290;
|
|
|
|
bufferRow.width = 460;
|
|
}
|
|
|
|
if (card.type === 'WEAPON') {
|
|
bufferText.width = 470;
|
|
bufferText.height = 250;
|
|
|
|
bufferRow.width = 470;
|
|
}
|
|
|
|
var fontSize = sunwell.settings.bodyFontSize;
|
|
var lineHeight = sunwell.settings.bodyLineHeight;
|
|
var totalLength = card.text.replace(/<\/*.>/g, '').length;
|
|
var smallerFirstLine = false;
|
|
|
|
if (totalLength >= 80) {
|
|
fontSize = sunwell.settings.bodyFontSize * 0.9;
|
|
lineHeight = sunwell.settings.bodyLineHeight * 0.9;
|
|
}
|
|
|
|
if (totalLength >= 100) {
|
|
fontSize = sunwell.settings.bodyFontSize * 0.8;
|
|
lineHeight = sunwell.settings.bodyLineHeight * 0.8;
|
|
}
|
|
|
|
bufferRow.height = lineHeight;
|
|
|
|
|
|
if (totalLength >= 75 && card.type === 'SPELL') {
|
|
smallerFirstLine = true;
|
|
}
|
|
|
|
if(forceSmallerFirstLine){
|
|
smallerFirstLine = true;
|
|
}
|
|
|
|
if (card.type === 'WEAPON') {
|
|
bufferRowCtx.fillStyle = '#fff';
|
|
} else {
|
|
bufferRowCtx.fillStyle = '#000';
|
|
}
|
|
bufferRowCtx.textBaseline = 'hanging';
|
|
|
|
bufferRowCtx.font = fontSize + 'px "' + sunwell.settings.bodyFont + '", sans-serif';
|
|
|
|
spaceWidth = 3;
|
|
|
|
for (i = 0; i < words.length; i++) {
|
|
word = words[i];
|
|
|
|
chars = word.split('');
|
|
|
|
width = bufferRowCtx.measureText(word).width;
|
|
log('Next word: ' + word);
|
|
|
|
if (!manualBreak && (xPos + width > bufferRow.width || (smallerFirstLine && xPos + width > bufferRow.width * 0.8))) {
|
|
log((xPos + width) + ' > ' + bufferRow.width);
|
|
log('Calculated line break');
|
|
smallerFirstLine = false;
|
|
lineCount++;
|
|
r = finishLine(bufferTextCtx, bufferRow, bufferRowCtx, xPos, yPos, bufferText.width);
|
|
xPos = r[0];
|
|
yPos = r[1];
|
|
justLineBreak = true;
|
|
}
|
|
|
|
for (j = 0; j < chars.length; j++) {
|
|
char = chars[j];
|
|
|
|
log(char + ' ' + char.charCodeAt(0));
|
|
|
|
if (char.charCodeAt(0) === 10) {
|
|
if (justLineBreak) {
|
|
justLineBreak = false;
|
|
continue;
|
|
}
|
|
lineCount++;
|
|
r = finishLine(bufferTextCtx, bufferRow, bufferRowCtx, xPos, yPos, bufferText.width);
|
|
xPos = r[0];
|
|
yPos = r[1];
|
|
log('Manual line break');
|
|
continue;
|
|
}
|
|
|
|
justLineBreak = false;
|
|
|
|
if (char === '<') {
|
|
if (chars[j + 1] === '/') {
|
|
if (chars[j + 2] === 'b') {
|
|
isBold--;
|
|
j += 3;
|
|
}
|
|
|
|
if (chars[j + 2] === 'i') {
|
|
isItalic--;
|
|
j += 3;
|
|
}
|
|
} else {
|
|
if (chars[j + 1] === 'b') {
|
|
isBold++;
|
|
j += 2;
|
|
}
|
|
|
|
if (chars[j + 1] === 'i') {
|
|
isItalic++;
|
|
j += 2;
|
|
}
|
|
}
|
|
|
|
bufferRowCtx.font = (isBold > 0 ? 'bold ' : '') + (isItalic > 0 ? 'italic' : '') + ' ' + fontSize + 'px/1em "' + sunwell.settings.bodyFont + '", sans-serif';
|
|
continue;
|
|
}
|
|
|
|
bufferRowCtx.fillText(char, xPos + sunwell.settings.bodyFontOffset.x, sunwell.settings.bodyFontOffset.y);
|
|
|
|
xPos += bufferRowCtx.measureText(char).width + (spaceWidth / 8);
|
|
}
|
|
|
|
xPos += spaceWidth;
|
|
}
|
|
|
|
lineCount++;
|
|
finishLine(bufferTextCtx, bufferRow, bufferRowCtx, xPos, yPos, bufferText.width);
|
|
|
|
if(card.type === 'SPELL' && lineCount === 4){
|
|
if(!smallerFirstLine && !forceSmallerFirstLine){
|
|
drawBodyText(targetCtx, s, card, true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var b = contextBoundingBox(bufferTextCtx);
|
|
|
|
b.h = Math.ceil(b.h / bufferRow.height) * bufferRow.height;
|
|
|
|
targetCtx.drawImage(bufferText, b.x, b.y - 2, b.w, b.h, (centerLeft - (b.w / 2)) * s, (centerTop - (b.h / 2)) * s, b.w * s, (b.h + 2) * s);
|
|
|
|
if (sunwell.settings.debug) {
|
|
targetCtx.save();
|
|
targetCtx.strokeStyle = 'green';
|
|
targetCtx.beginPath();
|
|
targetCtx.rect((centerLeft - (b.w / 2)) * s, (centerTop - (b.h / 2)) * s, b.w * s, (b.h + 2) * s);
|
|
targetCtx.stroke();
|
|
targetCtx.strokeStyle = 'red';
|
|
targetCtx.beginPath();
|
|
targetCtx.rect((centerLeft - (bufferText.width / 2)) * s, (centerTop - (bufferText.height / 2)) * s, bufferText.width * s, (bufferText.height + 2) * s);
|
|
targetCtx.stroke();
|
|
targetCtx.restore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a curve and t, the function returns the point on the curve.
|
|
* r is the rotation of the point in radians.
|
|
* @param curve
|
|
* @param t
|
|
* @returns {{x: (number|*), y: (number|*), r: number}}
|
|
*/
|
|
function getPointOnCurve(curve, t) {
|
|
|
|
var rX, rY, x, y;
|
|
|
|
rX = 3 * Math.pow(1 - t, 2) * (curve[1].x - curve[0].x) + 6 * (1 - t) * t * (curve[2].x - curve[1].x) + 3 * Math.pow(t, 2) * (curve[3].x - curve[2].x);
|
|
rY = 3 * Math.pow(1 - t, 2) * (curve[1].y - curve[0].y) + 6 * (1 - t) * t * (curve[2].y - curve[1].y) + 3 * Math.pow(t, 2) * (curve[3].y - curve[2].y);
|
|
|
|
x = Math.pow((1 - t), 3) * curve[0].x + 3 * Math.pow((1 - t), 2) * t * curve[1].x + 3 * (1 - t) * Math.pow(t, 2) * curve[2].x + Math.pow(t, 3) * curve[3].x;
|
|
y = Math.pow((1 - t), 3) * curve[0].y + 3 * Math.pow((1 - t), 2) * t * curve[1].y + 3 * (1 - t) * Math.pow(t, 2) * curve[2].y + Math.pow(t, 3) * curve[3].y;
|
|
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
r: Math.atan2(rY, rX)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prints the title of a card.
|
|
* @param title
|
|
*/
|
|
function drawCardTitle(targetCtx, s, card) {
|
|
var buffer = getBuffer();
|
|
var title = card.title;
|
|
buffer.width = 1024;
|
|
buffer.height = 200;
|
|
var ctx = buffer.getContext('2d');
|
|
var boundaries;
|
|
ctx.save();
|
|
|
|
var pathMiddle = .58;
|
|
var maxWidth = 580;
|
|
|
|
//Path midpoint at t = 0.56
|
|
var c = [
|
|
{x: 0, y: 110},
|
|
{x: 102, y: 137},
|
|
{x: 368, y: 16},
|
|
{x: 580, y: 100}
|
|
];
|
|
|
|
if (card.type === 'SPELL') {
|
|
pathMiddle = .52;
|
|
maxWidth = 580;
|
|
c = [
|
|
{x: 10, y: 100},
|
|
{x: 212, y: 35},
|
|
{x: 368, y: 35},
|
|
{x: 570, y: 105}
|
|
]
|
|
}
|
|
|
|
if (card.type === 'WEAPON') {
|
|
pathMiddle = .58;
|
|
maxWidth = 580;
|
|
c = [
|
|
{x: 10, y: 75},
|
|
{x: 50, y: 75},
|
|
{x: 500, y: 75},
|
|
{x: 570, y: 75}
|
|
]
|
|
}
|
|
|
|
var fontSize = 51;
|
|
|
|
ctx.lineWidth = 13;
|
|
ctx.strokeStyle = 'black';
|
|
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'middle';
|
|
|
|
var textWidth = maxWidth + 1;
|
|
var steps,
|
|
begin;
|
|
|
|
while (textWidth > maxWidth && fontSize > 10) {
|
|
fontSize -= 1;
|
|
ctx.font = fontSize + 'px Belwe';
|
|
textWidth = 0;
|
|
for (var i = 0; i < title.length; i++) {
|
|
textWidth += ctx.measureText(title[i]).width + 2;
|
|
}
|
|
|
|
textWidth *= 1.25;
|
|
}
|
|
|
|
textWidth = textWidth / maxWidth;
|
|
begin = pathMiddle - (textWidth / 2);
|
|
steps = textWidth / title.length;
|
|
|
|
if (sunwell.settings.debug) {
|
|
ctx.save();
|
|
ctx.strokeStyle = 'red';
|
|
ctx.lineWidth = 5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(c[0].x, c[0].y);
|
|
ctx.bezierCurveTo(c[1].x, c[1].y, c[2].x, c[2].y, c[3].x, c[3].y);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
|
|
var p, t, leftPos = 0, m;
|
|
for (i = 0; i < title.length; i++) {
|
|
if (leftPos === 0) {
|
|
t = begin + (steps * i);
|
|
p = getPointOnCurve(c, t);
|
|
leftPos = p.x;
|
|
} else {
|
|
t += 0.01;
|
|
p = getPointOnCurve(c, t);
|
|
while (p.x < leftPos) {
|
|
t += 0.001;
|
|
p = getPointOnCurve(c, t);
|
|
}
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.translate(p.x, p.y);
|
|
|
|
ctx.scale(1.2, 1);
|
|
//ctx.setTransform(1.2, p.r, 0, 1, p.x, p.y);
|
|
ctx.rotate(p.r);
|
|
|
|
ctx.lineWidth = 10 * (fontSize / 50);
|
|
ctx.strokeStyle = 'black';
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillText(title[i], 0, 0);
|
|
ctx.strokeText(title[i], 0, 0);
|
|
m = ctx.measureText(title[i]).width * 1.25;
|
|
leftPos += m;
|
|
|
|
if (['i', 'f'].indexOf(title[i]) !== -1) {
|
|
leftPos += m * 0.1;
|
|
}
|
|
|
|
ctx.fillStyle = 'white';
|
|
ctx.strokeStyle = 'white';
|
|
ctx.lineWidth = 2.5 * (fontSize / 50);
|
|
ctx.fillText(title[i], 0, 0);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
targetCtx.drawImage(
|
|
buffer,
|
|
0,
|
|
0,
|
|
580,
|
|
200,
|
|
|
|
(395 - (580 / 2)) * s,
|
|
(725 - 200) * s,
|
|
580 * s,
|
|
200 * s
|
|
);
|
|
}
|
|
|
|
function draw(getAsset, cvs, ctx, card, s, callback, internalCB) {
|
|
|
|
var sw = card.sunwell,
|
|
t,
|
|
drawTimeout,
|
|
drawProgress = 0,
|
|
renderStart = Date.now();
|
|
|
|
drawTimeout = setTimeout(function () {
|
|
log('Drawing timeout at point ' + drawProgress + ' in ' + card.title);
|
|
log(card);
|
|
internalCB();
|
|
if (callback) {
|
|
callback(cvs);
|
|
}
|
|
}, 5000);
|
|
|
|
if (typeof card.texture === 'string') {
|
|
t = getAsset(card.texture);
|
|
} else {
|
|
if (card.texture instanceof Image) {
|
|
t = card.texture;
|
|
} else {
|
|
t = getBuffer();
|
|
t.width = card.texture.crop.w;
|
|
t.height = card.texture.crop.h;
|
|
(function () {
|
|
var tCtx = t.getContext('2d');
|
|
tCtx.drawImage(card.texture.image, card.texture.crop.x, card.texture.crop.y, card.texture.crop.w, card.texture.crop.h, 0, 0, t.width, t.height);
|
|
})();
|
|
}
|
|
}
|
|
|
|
if (!t) {
|
|
t = getAsset('~');
|
|
}
|
|
|
|
drawProgress = 2;
|
|
|
|
ctx.save();
|
|
ctx.clearRect(0, 0, cvs.width, cvs.height);
|
|
|
|
drawProgress = 3;
|
|
|
|
ctx.save();
|
|
if (card.type === 'MINION') {
|
|
drawEllipse(ctx, 180 * s, 75 * s, 430 * s, 590 * s);
|
|
ctx.clip();
|
|
ctx.fillStyle = 'grey';
|
|
ctx.fillRect(0, 0, 765 * s, 1100 * s);
|
|
ctx.drawImage(t, 0, 0, t.width, t.height, 100 * s, 75 * s, 590 * s, 590 * s);
|
|
}
|
|
|
|
if (card.type === 'SPELL') {
|
|
ctx.rect(125 * s, 165 * s, 529 * s, 434 * s);
|
|
ctx.clip();
|
|
ctx.fillStyle = 'grey';
|
|
ctx.fillRect(0, 0, 765 * s, 1100 * s);
|
|
ctx.drawImage(t, 0, 0, t.width, t.height, 125 * s, 117 * s, 529 * s, 529 * s);
|
|
}
|
|
|
|
if (card.type === 'WEAPON') {
|
|
drawEllipse(ctx, 150 * s, 135 * s, 476 * s, 468 * s);
|
|
ctx.clip();
|
|
ctx.fillStyle = 'grey';
|
|
ctx.fillRect(0, 0, 765 * s, 1100 * s);
|
|
ctx.drawImage(t, 0, 0, t.width, t.height, 150 * s, 135 * s, 476 * s, 476 * s);
|
|
}
|
|
ctx.restore();
|
|
|
|
drawProgress = 4;
|
|
|
|
ctx.drawImage(getAsset(sw.cardBack), 0, 0, 764, 1100, 0, 0, cvs.width, cvs.height);
|
|
|
|
drawProgress = 5;
|
|
|
|
if(card.costHealth){
|
|
ctx.drawImage(getAsset('health'), 0, 0, 167, 218, 24 * s, 62 * s, 167 * s, 218 * s);
|
|
ctx.save();
|
|
ctx.shadowBlur=50*s;
|
|
ctx.shadowColor='#FF7275';
|
|
ctx.shadowOffsetX = 1000;
|
|
ctx.globalAlpha = .5;
|
|
ctx.drawImage(getAsset('health'), 0, 0, 167, 218, (24 * s)-1000, 62 * s, 167 * s, 218 * s);
|
|
ctx.restore();
|
|
} else {
|
|
ctx.drawImage(getAsset('gem'), 0, 0, 182, 180, 24 * s, 82 * s, 182 * s, 180 * s);
|
|
}
|
|
|
|
drawProgress = 6;
|
|
|
|
if (card.type === 'MINION') {
|
|
if (sw.rarity) {
|
|
ctx.drawImage(getAsset(sw.rarity), 0, 0, 146, 146, 326 * s, 607 * s, 146 * s, 146 * s);
|
|
}
|
|
|
|
ctx.drawImage(getAsset('title'), 0, 0, 608, 144, 94 * s, 546 * s, 608 * s, 144 * s);
|
|
|
|
if (card.race) {
|
|
ctx.drawImage(getAsset('race'), 0, 0, 529, 106, 125 * s, 937 * s, 529 * s, 106 * s);
|
|
}
|
|
|
|
ctx.drawImage(getAsset('attack'), 0, 0, 214, 238, 0, 862 * s, 214 * s, 238 * s);
|
|
ctx.drawImage(getAsset('health'), 0, 0, 167, 218, 575 * s, 876 * s, 167 * s, 218 * s);
|
|
|
|
if (card.rarity === 'LEGENDARY') {
|
|
ctx.drawImage(getAsset('dragon'), 0, 0, 569, 417, 196 * s, 0, 569 * s, 417 * s);
|
|
}
|
|
}
|
|
|
|
drawProgress = 7;
|
|
|
|
if (card.type === 'SPELL') {
|
|
if (sw.rarity) {
|
|
ctx.drawImage(getAsset(sw.rarity), 0, 0, 149, 149, 311 * s, 607 * s, 150 * s, 150 * s);
|
|
}
|
|
|
|
ctx.drawImage(getAsset('title-spell'), 0, 0, 646, 199, 66 * s, 530 * s, 646 * s, 199 * s);
|
|
}
|
|
|
|
if (card.type === 'WEAPON') {
|
|
if (sw.rarity) {
|
|
ctx.drawImage(getAsset(sw.rarity), 0, 0, 146, 144, 315 * s, 592 * s, 146 * s, 144 * s);
|
|
}
|
|
|
|
ctx.drawImage(getAsset('title-weapon'), 0, 0, 660, 140, 56 * s, 551 * s, 660 * s, 140 * s);
|
|
|
|
ctx.drawImage(getAsset('swords'), 0, 0, 312, 306, 32 * s, 906 * s, 187 * s, 183 * s);
|
|
ctx.drawImage(getAsset('shield'), 0, 0, 301, 333, 584 * s, 890 * s, 186 * s, 205 * s);
|
|
}
|
|
|
|
drawProgress = 8;
|
|
|
|
|
|
if (card.set !== 'CORE') {
|
|
(function () {
|
|
var xPos;
|
|
|
|
if (card.type === 'SPELL') {
|
|
xPos = 265;
|
|
}
|
|
|
|
if (card.type === 'MINION') {
|
|
xPos = 265;
|
|
}
|
|
|
|
if (card.race && card.type === 'MINION') {
|
|
ctx.drawImage(getAsset(sw.bgLogo), 0, 0, 281, 244, xPos * s, 734 * s, (281 * 0.95) * s, (244 * 0.95) * s);
|
|
} else {
|
|
if (card.type === 'SPELL') {
|
|
ctx.drawImage(getAsset(sw.bgLogo), 0, 0, 281, 244, xPos * s, 740 * s, 253 * s, 220 * s);
|
|
} else {
|
|
ctx.drawImage(getAsset(sw.bgLogo), 0, 0, 281, 244, xPos * s, 734 * s, 281 * s, 244 * s);
|
|
}
|
|
|
|
}
|
|
})();
|
|
}
|
|
|
|
drawProgress = 9;
|
|
|
|
|
|
drawProgress = 10;
|
|
|
|
drawNumber(ctx, 116, 170, s, card.cost || 0, 170, card.costStyle);
|
|
|
|
drawProgress = 11;
|
|
|
|
drawCardTitle(ctx, s, card);
|
|
|
|
drawProgress = 12;
|
|
|
|
if (card.type === 'MINION') {
|
|
if (card.race) {
|
|
renderRaceText(ctx, s, card);
|
|
}
|
|
|
|
drawNumber(ctx, 128, 994, s, card.attack || 0, 150, card.attackStyle);
|
|
drawNumber(ctx, 668, 994, s, card.health || 0, 150, card.healthStyle);
|
|
}
|
|
|
|
drawProgress = 13;
|
|
|
|
if (card.type === 'WEAPON') {
|
|
drawNumber(ctx, 128, 994, s, card.attack || 0, 150, card.attackStyle);
|
|
drawNumber(ctx, 668, 994, s, card.durability || 0, 150, card.durabilityStyle);
|
|
}
|
|
|
|
drawProgress = 14;
|
|
|
|
if (!card.silenced || card.type !== 'MINION') {
|
|
drawBodyText(ctx, s, card);
|
|
} else {
|
|
ctx.drawImage(getAsset('silence-x'), 0, 0, 410, 397, 200 * s, 660 * s, 410 * s, 397 * s);
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
clearTimeout(drawTimeout);
|
|
|
|
log('Rendertime: ' + (Date.now() - renderStart) + 'ms');
|
|
|
|
internalCB();
|
|
if (callback) {
|
|
callback(cvs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the HS-API object, you pass to this function.
|
|
* @return Canvas
|
|
*/
|
|
function render(card, resolution, callback) {
|
|
log('Preparing assets for: ' + card.title);
|
|
|
|
var cvs = getBuffer(),
|
|
ctx = cvs.getContext('2d'),
|
|
s = (resolution || 512) / 764,
|
|
loadList = ['silence-x'];
|
|
|
|
cvs.width = resolution || 512;
|
|
cvs.height = Math.round(cvs.width * 1.4397905759);
|
|
|
|
card.sunwell = card.sunwell || {};
|
|
|
|
card.sunwell.cardBack = card.type.substr(0, 1).toLowerCase() + card.playerClass.substr(0, 1) + card.playerClass.substr(1).toLowerCase();
|
|
|
|
loadList.push(card.sunwell.cardBack);
|
|
|
|
loadList.push('gem');
|
|
|
|
if (card.type === 'MINION') {
|
|
loadList.push('attack', 'health', 'title');
|
|
|
|
if (card.rarity === 'LEGENDARY') {
|
|
loadList.push('dragon');
|
|
}
|
|
|
|
if (card.rarity !== 'FREE' && !(card.rarity === 'COMMON' && card.set === 'CORE')) {
|
|
card.sunwell.rarity = 'rarity-' + card.rarity.toLowerCase();
|
|
loadList.push(card.sunwell.rarity);
|
|
}
|
|
}
|
|
|
|
if (card.type === 'SPELL') {
|
|
loadList.push('attack', 'health', 'title-spell');
|
|
|
|
if (card.rarity !== 'FREE' && !(card.rarity === 'COMMON' && card.set === 'CORE')) {
|
|
card.sunwell.rarity = 'spell-rarity-' + card.rarity.toLowerCase();
|
|
loadList.push(card.sunwell.rarity);
|
|
}
|
|
}
|
|
|
|
if (card.type === 'WEAPON') {
|
|
loadList.push('swords', 'shield', 'title-weapon');
|
|
|
|
if (card.rarity !== 'FREE' && !(card.rarity === 'COMMON' && card.set === 'CORE')) {
|
|
card.sunwell.rarity = 'weapon-rarity-' + card.rarity.toLowerCase();
|
|
loadList.push(card.sunwell.rarity);
|
|
}
|
|
}
|
|
|
|
|
|
if (['BRM', 'GVG', 'LOE', 'NAX', 'TGT', 'OG'].indexOf(card.set) === -1) {
|
|
card.sunwell.bgLogo = 'bg-cl';
|
|
} else {
|
|
card.sunwell.bgLogo = 'bg-' + card.set.toLowerCase();
|
|
}
|
|
|
|
if (card.type === 'SPELL') {
|
|
card.sunwell.bgLogo = 'spell-' + card.sunwell.bgLogo;
|
|
}
|
|
|
|
loadList.push(card.sunwell.bgLogo);
|
|
|
|
if (card.race) {
|
|
loadList.push('race');
|
|
}
|
|
|
|
|
|
if (typeof card.texture === 'string' && card.set !== 'CHEAT') {
|
|
if (s <= .5) {
|
|
loadList.push('h:' + card.texture);
|
|
} else {
|
|
loadList.push('t:' + card.texture);
|
|
}
|
|
}
|
|
|
|
log('Assets prepared, now loading');
|
|
|
|
fetchAssets(loadList)
|
|
.then(function (assets) {
|
|
log('Assets loaded for: ' + card.title);
|
|
|
|
function getAsset(id) {
|
|
return assets[id] || getMissingImg(id);
|
|
}
|
|
|
|
draw(getAsset, cvs, ctx, card, s, callback, function () {
|
|
log('Card rendered: ' + card.title);
|
|
});
|
|
})
|
|
.catch(function(err) {
|
|
console.error(err.stack);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a new card object that can also be manipulated at a later point.
|
|
* Provide an image object as render target as output for the visual card data.
|
|
* @param settings
|
|
* @param width
|
|
* @param renderTarget
|
|
*/
|
|
sunwell.createCard = function (settings, width, callback) {
|
|
if (!settings) {
|
|
throw new Error('No card object given');
|
|
}
|
|
|
|
//Make compatible to tech cards
|
|
if (validRarity.indexOf(settings.rarity) === -1) {
|
|
settings.rarity = 'FREE';
|
|
}
|
|
|
|
//Make compatible to hearthstoneJSON format.
|
|
if (settings.title === undefined) {
|
|
settings.title = settings.name;
|
|
}
|
|
if (settings.gameId === undefined) {
|
|
settings.gameId = settings.id;
|
|
}
|
|
|
|
if (sunwell.settings.idAsTexture) {
|
|
settings.texture = settings.gameId;
|
|
}
|
|
|
|
settings.costStyle = settings.costStyle || '0';
|
|
settings.healthStyle = settings.healthStyle || '0';
|
|
settings.attackStyle = settings.attackStyle || '0';
|
|
settings.durabilityStyle = settings.durabilityStyle || '0';
|
|
|
|
settings.silenced = settings.silenced || false;
|
|
settings.costHealth = settings.costHealth || false;
|
|
|
|
settings.width = width;
|
|
|
|
log('Queried render: ' + settings.title);
|
|
|
|
render(settings, width, function (result) {
|
|
result.toBuffer(function(err, buffer) {
|
|
if (callback) {
|
|
callback(err, buffer);
|
|
}
|
|
});
|
|
});
|
|
|
|
return {
|
|
redraw: function (callback) {
|
|
render(settings, width, function (result) {
|
|
result.toBuffer(function(err, buffer) {
|
|
if (callback) {
|
|
callback(err, buffer);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
update: function (properties, callback) {
|
|
for (var key in properties) {
|
|
settings[key] = properties[key];
|
|
}
|
|
|
|
cacheKey = checksum(settings);
|
|
|
|
render(settings, width, function (result) {
|
|
result.toBuffer(function(err, buffer) {
|
|
if (callback) {
|
|
callback(err, buffer);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
return sunwell;
|
|
};
|