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 * @author Matthias Klein (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; };