diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61d9132 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules + +# dotenv environment variable files +.env + +**/dist/**/* +static/dist diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..40f11ab --- /dev/null +++ b/deno.json @@ -0,0 +1,42 @@ +{ + "lock": false, + "tasks": { + "dev": "deno run -A tools/dev.ts", + "build": "deno run -A tools/build.ts", + "build:watch": "deno run --watch=src -A tools/build.ts", + "serve": "deno run -A src/server/index.ts", + "serve:watch": "deno run --watch=src -A src/server/index.ts" + }, + "lint": { + "rules": { + "tags": [ + "recommended" + ] + } + }, + "exclude": [ + "dist" + ], + "imports": { + "react/": "https://esm.sh/react@18.3.1/", + "react-dom/": "https://esm.sh/react-dom@18.3.1/client/", + "react": "https://esm.sh/react@18.3.1", + "react-dom": "https://esm.sh/react-dom@18.3.1/client", + "canvas": "https://esm.sh/canvas@3.0.0" + }, + "compilerOptions": { + "lib": ["deno.ns", "DOM"], + "jsx": "react-jsx", + "jsxImportSource": "react", + "strict": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "allowUnusedLabels": false + }, + "fmt": { + "useTabs": true + } +} diff --git a/reference.js b/reference.js new file mode 100644 index 0000000..6f80e66 --- /dev/null +++ b/reference.js @@ -0,0 +1,1230 @@ +// https://github.com/shardofhonor/dominion-card-generator/blob/master/docs/main.js + +let templateSize = 0; //save globally + +let useCORS = true; // flag to activate loading of external images via CORS helper function -> otherwise canvas is tainted and download button not working +//const CORS_ANYWHERE_BASE_URL = 'https://dominion-card-generator-cors.herokuapp.com/'; +//const CORS_ANYWHERE_BASE_URL = 'https://thingproxy.freeboard.io/fetch/'; +const CORS_ANYWHERE_BASE_URL = 'https://proxy.cors.sh/'; // from https://blog.grida.co/cors-anywhere-for-everyone-free-reliable-cors-proxy-service-73507192714e + + +// Initialization of complete logic on load of page +function initCardImageGenerator() { + + //these three can all be expanded as you see fit + var icons = { //the names should match the image filenames (plus a .png extension). + "@": ["Debt", "white", "Treasure"], + "\\^": ["Potion", "white", "Treasure"], + "%": ["VP", "white", "Victory"], + "#": ["VP-Token", "white", "Victory"], //German VP Token (not a nice decision of ASS Altenburger, but maybe nice to have to keep the cards consistent) + "\\$": ["Coin", "black", "Treasure"], + "§": ["Custom Icon", "white", "Treasure"] + }; + var normalColorFactorLists = [ + ["Action/Event", [1, 1, 1]], + ["Treasure", [1.1, 0.95, 0.55]], + ["Victory", [0.75, 0.9, 0.65]], + ["Reaction", [0.65, 0.8, 1.05]], + ["Duration", [1.2, 0.8, 0.4]], + ["Reserve", [0.9, 0.75, 0.5]], + ["Curse", [0.85, 0.6, 1.1]], + ["Shelter", [1.05, 0.65, 0.5]], + ["Ruins", [0.75, 0.6, 0.35]], + ["Landmark", [0.45, 1.25, 0.85]], + ["Night", [0.3, 0.4, 0.45]], + ["Boon", [1.4, 1.35, 0.55, 0, 0, 0, 1.7, 1.25, 0.65, 1.95, 1.6, 0.4]], + ["Hex", [0.75, 0.6, 2.1, 0, 0, 0, 0.8, 0.8, 0.8, 1.0, 0.75, 2.1]], + ["State", [1.1, 1.3, 1.3, 0.6, 0.15, 0, 1.55, 1.15, 1.05, 1.4, 0.65, 0.45]], + ["Artifact", [1.15, 1, 0.75, 0.3, 0.15, 0.05]], + ["Project", [1.15, 0.95, 0.9, 0.4, 0.2, 0.15]], + ["Way", [1, 1.15, 1.25, 0.25, 0.3, 0.35, 1.6, 1.6, 1.6, 1.3, 1.3, 1.3]], + ["Ally", [1, 0.95, 0.85, 0.35, 0.3, 0.15, 0.9, 0.8, 0.7, 0.9, 0.8, 0.7]], + ["Trait", [0.95, 0.8, 1.1, 0.3, 0.25, 0.35, 1.6, 1.6, 1.6, 1.3, 1.3, 1.3]] + ]; + var boldableKeywords = [ //case-insensitive + "card", + "buy", + "action", + "coffer", + "villager", + + "aktion", + "aktionen", + "karte", + "karten", + "kauf", + "käufe", + "dorfbewohner", + "münze", + "münzen" + ]; + var specialBoldableKeywords = [ + "favor", + "gefallen" + ]; + var travellerTypesPattern = new RegExp(["Traveller", "Traveler", "Reisender", "Reisende", "Reiziger", "Matkaaja", "Itinérant", "Путешественник", "Приключенец"].join("|")); + + + var normalColorCustomIndices = [0, 0]; + var normalColorDropdowns = document.getElementsByName("normalcolor"); + for (var j = 0; j < normalColorDropdowns.length; ++j) { + for (var i = 0; i < normalColorFactorLists.length; ++i) { //"- j" because only the first dropdown should have Night + var option = document.createElement("option"); + option.textContent = normalColorFactorLists[i][0]; + normalColorDropdowns[j].appendChild(option); + } + normalColorCustomIndices[j] = normalColorDropdowns[j].childElementCount; + var customOption = document.createElement("option"); + customOption.textContent = "CUSTOM"; + normalColorDropdowns[j].appendChild(customOption); + customOption = document.createElement("option"); + customOption.textContent = "EXTRA CUSTOM"; + normalColorDropdowns[j].appendChild(customOption); + normalColorDropdowns[j].selectedIndex = 0; + } + //var templateSize = 0; + + function rebuildBoldLinePatternWords() { + let elemBoldkeys = document.getElementById("boldkeys"); + let customBoldableKeywords = elemBoldkeys !== null ? elemBoldkeys.value : ""; + let boldableKeywordsFull = customBoldableKeywords.length > 0 ? boldableKeywords.concat(customBoldableKeywords.split(";")) : boldableKeywords; + boldableKeywordsFull.forEach(function (word, index) { + this[index] = word.trim(); + }, boldableKeywordsFull); + boldLinePatternWords = RegExp("(?:([-+]\\d+)\\s+|(\\+))(" + boldableKeywordsFull.join("|") + "s?)", "ig"); + boldLinePatternWordsSpecial = RegExp("(?:([-+]\\d+)\\s+|(?:(\\d+)\\s+)|(\\+)|)(" + specialBoldableKeywords.join("|") + "s?)", "ig"); + } + var boldLinePatternWords; + var boldLinePatternWordsSpecial; + rebuildBoldLinePatternWords(); + + var iconList = "[" + Object.keys(icons).join("") + "]"; + //var boldLinePatternIcons = RegExp("[-+]\\d+\\s" + iconList + "\\d+", "ig"); + var iconWithNumbersPattern = "[-+]?(" + iconList + ")([\\d\\?]*[-+\\*]?)"; + var iconWithNumbersPatternSingle = RegExp("^([-+]?\\d+)?" + iconWithNumbersPattern + "(\\S*)$"); + iconWithNumbersPattern = RegExp(iconWithNumbersPattern, "g"); + + var canvases = document.getElementsByClassName("myCanvas"); + + var images = []; + var imagesLoaded = false; + var recolorFactorList = [ + [0.75, 1.1, 1.35, 0, 0, 0, 1, 2, 3, 4, 5, 6], + [0.75, 1.1, 1.35, 0, 0, 0, 1, 2, 3, 4, 5, 6] + ]; + + var normalColorCurrentIndices = [0, 0]; + var recoloredImages = []; + + function draw() { + + function getRecoloredImage(imageID, colorID, offset) { + if (!recoloredImages[imageID]) { //http://stackoverflow.com/questions/1445862/possible-to-use-html-images-like-canvas-with-getimagedata-putimagedata + var cnvs = document.createElement("canvas"); + var w = images[imageID].width, + h = images[imageID].height; + cnvs.width = w; + cnvs.height = h; + var ctx = cnvs.getContext("2d"); + ctx.drawImage(images[imageID], 0, 0); + + var imgdata = ctx.getImageData(0, 0, w, h); + var rgba = imgdata.data; + + offset = offset || 0; + var recolorFactors; + if (normalColorCurrentIndices[colorID] === normalColorCustomIndices[colorID]) + recolorFactors = recolorFactorList[colorID].slice(0, 3); + else if (normalColorCurrentIndices[colorID] > normalColorCustomIndices[colorID]) + recolorFactors = recolorFactorList[colorID]; + else + recolorFactors = normalColorFactorLists[normalColorCurrentIndices[colorID] - colorID][1]; + recolorFactors = recolorFactors.slice(); + + while (recolorFactors.length < 6) + recolorFactors.push(0); + + if (offset == 0) { + for (var ch = 0; ch < 3; ++ch) + recolorFactors[ch] -= recolorFactors[ch + 3]; + for (var px = 0, ct = w * h * 4; px < ct; px += 4) + if (rgba[px + 3]) //no need to recolor pixels that are fully transparent + for (var ch = 0; ch < 3; ++ch) + rgba[px + ch] = Math.max(0, Math.min(255, Math.round(recolorFactors[ch + 3] * 255 + rgba[px + ch] * recolorFactors[ch]))); + } else { + while (recolorFactors.length < 12) + recolorFactors.push(genericCustomAccentColors[templateSize & 1][recolorFactors.length]); + for (var px = 0, ct = w * h * 4; px < ct; px += 4) + if (rgba[px + 3]) + for (var ch = 0; ch < 3; ++ch) + rgba[px + ch] = Math.max(0, Math.min(255, rgba[px + ch] * recolorFactors[ch + offset])); + } + + ctx.putImageData(imgdata, 0, 0); + recoloredImages[imageID] = cnvs; + } + return recoloredImages[imageID]; + } + + var iconReplacedWithSpaces = " "; + + function getWidthOfLineWithIconsReplacedWithSpaces(line) { + return context.measureText(line.replace(iconWithNumbersPattern, iconReplacedWithSpaces)).width; + } + + function getIconListing(icon) { + return icons[icon] || icons["\\" + icon]; + } + var shadowDistance = 10; + var italicSubstrings = ["[i]", "Heirloom: ", "Erbstück: ", "(This is not in the Supply.)", "Keep this until Clean-up."]; + + function writeLineWithIconsReplacedWithSpaces(line, x, y, scale, family, boldSize) { + boldSize = boldSize || 64; + context.textAlign = "left"; + + if (italicSubstrings.some(substring => line.includes(substring))) { + context.font = "italic " + context.font; + if (line.includes("[i]")) { + line = line.split("[i]").join(""); + x += boldSize * scale; + } + } else { + context.font = context.font.replace("italic ", ""); + } + + var words = line.split(" "); + for (var i = 0; i < words.length; ++i) { + var word = words[i]; + context.save(); + while (word) { + var match = word.match(iconWithNumbersPatternSingle); + if (match) { + var familyOriginal = family; + family = "mySpecials"; + var localY = y; + var localScale = scale; + if (words.length === 1 && !word.startsWith('+')) { + localY += 115 - scale * 48; + context.font = "bold 192pt " + family; + localScale = 1.6; + if (templateSize === 3) { + context.font = "bold 222pt " + family; + if (word.includes('$')) { // Treasure Base cards + localScale = localScale * 2; + } else { + localScale = localScale * 1.5; + } + } else { + x = x + 48 * scale; + } + } + var halfWidthOfSpaces = context.measureText(iconReplacedWithSpaces).width / 2 + 2; + + var image = false; + var iconKeys = Object.keys(icons); + for (var j = 0; j < iconKeys.length; ++j) { + if (iconKeys[j].replace("\\", "") == match[2]) { + image = images[numberFirstIcon + j]; + break; + } + } + + context.save(); + if (!match[1] && (match[0].charAt(0) === '+' || match[0].charAt(0) === '-')) { + match[1] = match[0].charAt(0); + } + if (match[1]) { + if (context.font[0] !== "b") + context.font = "bold " + context.font; + context.fillText(match[1], x, localY); + x += context.measureText(match[1]).width + 10 * localScale; + } + + x += halfWidthOfSpaces; + + context.translate(x, localY); + context.scale(localScale, localScale); + if (image && image.height) { //exists + //context.shadowColor = "#000"; + context.shadowBlur = 25; + context.shadowOffsetX = localScale * shadowDistance; + context.shadowOffsetY = localScale * shadowDistance; + context.drawImage(image, image.width / -2, image.height / -2); + context.shadowColor = "transparent"; + } //else... well, that's pretty weird, but so it goes. + if (match[3]) { //text in front of image + context.textAlign = "center"; + context.fillStyle = getIconListing(match[2])[1]; + let cost = match[3]; + let bigNumberScale = 1; + let nx = localScale > 1.4 ? 0 : -5 * localScale ^ 2; + let ny = localScale > 1 ? 6 * localScale : localScale > 0.7 ? 12 * localScale : localScale > 0.5 ? 24 * localScale : 48 * localScale; + if (localScale > 3) { + bigNumberScale = 0.8; + ny -= (115 * 0.2) / 2; + } + if (cost.length >= 2) { + // special handling for overpay and variable costs + let specialCost = cost.slice(-1); + let specialCostSize = 45; + let syShift = 0; + if (specialCost === '*') { + //specialCost = '✱'; + specialCostSize = 65; + syShift = 10; + if (cost.length > 2) { + bigNumberScale = 1.5 / (cost.length - 1); + } + } else if (specialCost === '+') { + specialCost = '✚'; + specialCostSize = 40; + if (cost.length > 2) { + bigNumberScale = 1.5 / (cost.length - 1); + } + } else { + specialCost = null; + bigNumberScale = 1.5 / cost.length; + } + if (specialCost != null) { + cost = cost.slice(0, -1) + " "; + context.font = "bold " + specialCostSize + "pt " + family; + let sx = localScale > 1 ? 45 / 2 * localScale : 45 * localScale; + let sy = localScale > 1 ? -20 * localScale : 12 * localScale - 35 * localScale; + if (cost.length >= 3) { + nx -= specialCostSize * 1 / 3; + sx += specialCostSize * 1 / 3; + } + sy += syShift * localScale; + context.fillText(specialCost, sx, sy); + } + } + context.font = "bold " + 115 * bigNumberScale + "pt " + family; + context.fillText(cost, nx, ny); + //context.strokeText(match[3], 0, 0); + } + context.restore(); + family = familyOriginal; + + x += halfWidthOfSpaces; + word = match[4]; + } else { + if (word.match(boldLinePatternWords) || word.match(boldLinePatternWordsSpecial)) { + if (words.length === 1) + context.font = "bold " + boldSize + "pt " + family; + else + context.font = "bold " + context.font; + } + if (context.font.includes('bold')) { + let lastChar = word.substr(word.length - 1); + if ([",", ";", ".", "?", "!", ":"].includes(lastChar)) { + word = word.slice(0, -1); + } else { + lastChar = ""; + } + context.fillText(word, x, y); + + if (lastChar != "") { + var x2 = context.measureText(word).width; + context.font = context.font.replace('bold ', ''); + context.fillText(lastChar, x + x2, y); + context.font = "bold " + context.font; + } + + word = word + lastChar; + } else { + context.fillText(word, x, y); + } + + break; //don't start this again + } + } + x += context.measureText(word + " ").width; + context.restore(); + } + } + + function writeSingleLine(line, x, y, maxWidth, initialSize, family) { + family = family || "myTitle"; + var size = (initialSize || 85) + 2; + do { + context.font = (size -= 2) + "pt " + family; + } while (maxWidth && getWidthOfLineWithIconsReplacedWithSpaces(line) > maxWidth); + writeLineWithIconsReplacedWithSpaces(line, x - getWidthOfLineWithIconsReplacedWithSpaces(line) / 2, y, size / 90, family); + } + + function writeDescription(elementID, xCenter, yCenter, maxWidth, maxHeight, boldSize) { + rebuildBoldLinePatternWords(); + var description = document.getElementById(elementID).value.replace(/ *\n */g, " \n ").replace(boldLinePatternWords, "$1\xa0$2$3").replace(boldLinePatternWordsSpecial, "$1$2\xa0$3$4") + " \n"; //separate newlines into their own words for easier processing + var words = description.split(" "); + var lines; + var widthsPerLine; + var heightsPerLine; + var overallHeight; + var size = 64 + 2; + do { //figure out the best font size, and also decide in advance how wide and tall each individual line is + widthsPerLine = []; + heightsPerLine = []; + overallHeight = 0; + + size -= 2; + context.font = size + "pt myText"; + var widthOfSpace = context.measureText(" ").width; + lines = []; + var line = ""; + var progressiveWidth = 0; + for (var i = 0; i < words.length; ++i) { + var word = words[i]; + var heightToAdd = 0; + if (word === "\n") { + lines.push(line); + if (line === "") //multiple newlines in a row + heightToAdd = size * 0.5; + else if (line === "-") //horizontal bar + heightToAdd = size * 0.75; + else if ((line.match(boldLinePatternWords) || line.match(boldLinePatternWordsSpecial)) && line.indexOf(" ") < 0) { //important line + heightToAdd = boldSize * 1.433; + var properFont = context.font; + context.font = "bold " + boldSize + "pt myText"; //resizing up to 64 + progressiveWidth = context.measureText(line).width; //=, not += + context.font = properFont; + } else if (line.match(iconWithNumbersPatternSingle) && !line.startsWith('+')) { + heightToAdd = 275; //192 * 1.433 + var properFont = context.font; + context.font = "bold 192pt myText"; + progressiveWidth = getWidthOfLineWithIconsReplacedWithSpaces(line); //=, not += + context.font = properFont; + } else //regular word + heightToAdd = size * 1.433; + line = ""; //start next line empty + widthsPerLine.push(progressiveWidth); + progressiveWidth = 0; + } else { + if (word.charAt(0) === "\xa0") { + word = word.substring(1); + } + if (progressiveWidth + getWidthOfLineWithIconsReplacedWithSpaces(" " + word) > maxWidth) { + lines.push(line + " "); + line = word; + heightToAdd = size * 1.433; + widthsPerLine.push(progressiveWidth); + progressiveWidth = getWidthOfLineWithIconsReplacedWithSpaces(word); + } else { + if (line.length) { + line += " "; + progressiveWidth += widthOfSpace; + } + line += word; + var properFont = context.font; + if (word.match(boldLinePatternWords) || word.match(boldLinePatternWordsSpecial)) //e.g. "+1 Action" + context.font = "bold " + properFont; + progressiveWidth += getWidthOfLineWithIconsReplacedWithSpaces(word); + context.font = properFont; + continue; + } + } + overallHeight += heightToAdd; + heightsPerLine.push(heightToAdd); + } + //overallHeight -= size*1.433; + } while (overallHeight > maxHeight && size > 16); //can only shrink so far before giving up + var y = yCenter - (overallHeight - size * 1.433) / 2; + //var barHeight = size / 80 * 10; + for (var i = 0; i < lines.length; ++i) { + var line = lines[i]; + if (line === "-") //horizontal bar + context.fillRect(xCenter / 2, y - size * 0.375 - 5, xCenter, 10); + else if (line.length) + writeLineWithIconsReplacedWithSpaces(line, xCenter - widthsPerLine[i] / 2, y, size / 96, "myText", boldSize); + //else empty line with nothing to draw + y += heightsPerLine[i]; + } + context.fillStyle = "black"; + } + + function writeIllustrationCredit(x, y, color, bold, size = 31) { + var illustrationCredit = document.getElementById("credit").value; + if (illustrationCredit) { + context.font = bold + size + "pt myText"; + context.fillStyle = color; + context.fillText(illustrationCredit, x, y); + context.fillStyle = "#000"; + } + } + + function writeCreatorCredit(x, y, color, bold, size = 31) { + var creatorCredit = document.getElementById("creator").value; + if (creatorCredit) { + context.textAlign = "right"; + context.font = bold + size + "pt myText"; + context.fillStyle = color; + context.fillText(creatorCredit, x, y); + context.fillStyle = "#000"; + } + } + + if (!imagesLoaded) { + imagesLoaded = (function () { + for (var i = 0; i < images.length; ++i) + if (!images[i].complete) { + return false; + } + return true; + })(); + if (!imagesLoaded) { + queueDraw(); + return; + } + } //else ready to draw! + + canvases[0].parentNode.setAttribute("data-status", "Redrawing..."); + + // clear + for (var i = 0; i < canvases.length; ++i) + canvases[i].getContext("2d").clearRect(0, 0, canvases[i].width, canvases[i].height); + + var context; + if (templateSize === 0 || templateSize === 2 || templateSize === 3) { + context = canvases[0].getContext("2d"); + } else if (templateSize === 1 || templateSize === 4) { + context = canvases[1].getContext("2d"); + } else { + context = canvases[2].getContext("2d"); + } + + //context.save(); + + // draw + + var picture = images[5]; + var pictureX = document.getElementById("picture-x").value; + var pictureY = document.getElementById("picture-y").value; + var pictureZoom = document.getElementById("picture-zoom").value; + var expansion = images[17]; + var typeLine = document.getElementById("type").value; + var heirloomLine = document.getElementById("type2").value; + var previewLine = document.getElementById("preview").value; + var priceLine = document.getElementById("price").value; + var numberPriceIcons = (priceLine.match(new RegExp("[" + Object.keys(icons).join("") + "]", "g")) || []).length + + var isEachColorDark = [false, false]; + for (var i = 0; i < 2; ++i) + isEachColorDark[i] = (i == 1 && normalColorCurrentIndices[1] == 0) ? isEachColorDark[0] : (((normalColorCurrentIndices[i] >= normalColorCustomIndices[i]) ? recolorFactorList[i] : normalColorFactorLists[normalColorCurrentIndices[i] - i][1]).slice(0, 3).reduce(function getSum(total, num) { + return total + parseFloat(num); + }) <= 1.5); + var differentIntensities = isEachColorDark[0] != isEachColorDark[1]; + + if (!(differentIntensities || parseInt(normalColorCurrentIndices[1]) == 0 || parseInt(normalColorCurrentIndices[0]) + 1 == parseInt(normalColorCurrentIndices[1]))) { + document.getElementById('color2splitselector').removeAttribute("style"); + } else { + document.getElementById('color2splitselector').setAttribute("style", "display:none"); + } + + function drawPicture(xCenter, yCenter, width, height) { + if (picture.height) { + var scale; + if (picture.width / width > picture.height / height) { //size of area to draw picture to + scale = height / picture.height; + } else { + scale = width / picture.width; + } + + let sizeX = picture.width * scale * pictureZoom; + let sizeY = picture.height * scale * pictureZoom; + let spaceX = sizeX - width; + let spaceY = sizeY - height; + let moveX = parseFloat(pictureX) * spaceX / 2; + let moveY = parseFloat(pictureY) * spaceY / 2; + + context.save(); + context.translate(xCenter + moveX, yCenter + moveY); + context.scale(scale * pictureZoom, scale * pictureZoom); + context.drawImage(picture, picture.width / -2, picture.height / -2); + context.restore(); + } + } + + function removeCorners(width, height, radius) { + context.clearRect(0, 0, radius, radius); + context.clearRect(width - radius, 0, radius, radius); + context.clearRect(0, height - radius, radius, radius); + context.clearRect(width - radius, height - radius, radius, radius); + } + + function drawExpansionIcon(xCenter, yCenter, width, height) { + if (expansion.height) { + var scale; + if (expansion.width / width < expansion.height / height) { //size of area to draw picture to + scale = height / expansion.height; + } else { + scale = width / expansion.width; + } + context.save(); + context.translate(xCenter, yCenter); + context.scale(scale, scale); + context.drawImage(expansion, expansion.width / -2, expansion.height / -2); + context.restore(); + } + } + + if (templateSize == 0) { //card + drawPicture(704, 706, 1150, 835); + removeCorners(1403, 2151, 100); + + context.drawImage(getRecoloredImage(0, 0), 0, 0); //CardColorOne + if (normalColorCurrentIndices[1] > 0) { //two colors are different + let splitPosition = document.getElementById("color2split").value; + if (splitPosition == 27) { + context.drawImage(getRecoloredImage(1, 1), 0, 0); //CardColorTwo - Half + context.drawImage(images[27], 0, 0); //CardColorThree + } else { + context.drawImage(getRecoloredImage(!differentIntensities ? splitPosition : 12, 1), 0, 0); //CardColorTwo + } + } + context.drawImage(getRecoloredImage(2, 0, 6), 0, 0); //CardGray + context.drawImage(getRecoloredImage(16, 0, 9), 0, 0); //CardBrown + if (normalColorCurrentIndices[0] > 0 && !isEachColorDark[0] && normalColorCurrentIndices[1] == 0) //single (non-Action, non-Night) color + context.drawImage(images[3], 44, 1094); //DescriptionFocus + + if (travellerTypesPattern.test(typeLine) || document.getElementById("traveller").checked) { + context.save(); + context.globalCompositeOperation = "luminosity"; + if (isEachColorDark[0]) + context.globalAlpha = 0.33; + context.drawImage(images[4], 524, 1197); //Traveller + context.restore(); + } + + context.textAlign = "center"; + context.textBaseline = "middle"; + //context.font = "small-caps" + context.font; + if (heirloomLine) { + context.drawImage(images[13], 97, 1720); //Heirloom banner + writeSingleLine(heirloomLine, 701, 1799, 1040, 58, "myText"); + } + if (isEachColorDark[1]) + context.fillStyle = "white"; + writeSingleLine(document.getElementById("title").value, 701, 215, previewLine ? 800 : 1180, 75); + if (typeLine.split(" - ").length >= 4) { + let types2 = typeLine.split(" - "); + let types1 = types2.splice(0, Math.ceil(types2.length / 2)); + let left = priceLine ? 750 + 65 * (numberPriceIcons - 1) : 701; + let right = priceLine ? 890 - 65 * (numberPriceIcons - 1) : 1180; + writeSingleLine(types1.join(" - ") + " -", left, 1922 - 26, right, 42); + writeSingleLine(types2.join(" - "), left, 1922 + 26, right, 42); + } else { + if (expansion.height > 0 && expansion.width > 0) { + let left = priceLine ? 730 + 65 * (numberPriceIcons - 1) : 701; + let right = priceLine ? 800 - 65 * (numberPriceIcons - 1) : 900; + writeSingleLine(typeLine, left, 1922, right, 64); + } else { + let left = priceLine ? 750 + 125 * (numberPriceIcons - 1) : 701; + let right = priceLine ? 890 - 85 * (numberPriceIcons - 1) : 1180; + writeSingleLine(typeLine, left, 1922, right, 64); + } + } + if (priceLine) + writeLineWithIconsReplacedWithSpaces(priceLine + " ", 153, 1940, 85 / 90, "mySpecials"); //adding a space confuses writeLineWithIconsReplacedWithSpaces into thinking this isn't a line that needs resizing + if (previewLine) { + writeSingleLine(previewLine += " ", 223, 210, 0, 0, "mySpecials"); + writeSingleLine(previewLine, 1203, 210, 0, 0, "mySpecials"); + } + context.fillStyle = (isEachColorDark[0]) ? "white" : "black"; + if (!heirloomLine) + writeDescription("description", 701, 1500, 960, 660, 64); + else + writeDescription("description", 701, 1450, 960, 560, 64); + writeIllustrationCredit(150, 2038, "white", ""); + writeCreatorCredit(1253, 2038, "white", ""); + + drawExpansionIcon(1230, 1920, 80, 80); + + } else if (templateSize == 1) { //event/landscape + drawPicture(1075, 584, 1887, 730); + removeCorners(2151, 1403, 100); + + if (document.getElementById("trait").checked) { + context.drawImage(getRecoloredImage(28, 0), 0, 0); //TraitColorOne + if (heirloomLine) + context.drawImage(images[14], 146, 832); //EventHeirloom + + context.drawImage(getRecoloredImage(29, 0, 6), 0, 0); //TraitUncoloredDetails + context.drawImage(getRecoloredImage(15, 0, 9), 0, 0); //EventBar + context.drawImage(getRecoloredImage(30, 0), 0, 0); //TraitColorSide + context.drawImage(getRecoloredImage(31, 0, 6), 0, 0); //TraitUncoloredDetailsSide + context.drawImage(getRecoloredImage(15, 0, 9), 0, 0); //EventBar + + } else { + + context.drawImage(getRecoloredImage(6, 0), 0, 0); //EventColorOne + if (heirloomLine) + context.drawImage(images[14], 146, 832); //EventHeirloom + if (normalColorCurrentIndices[1] > 0) //two colors are different + context.drawImage(getRecoloredImage(7, 1), 0, 0); //EventColorTwo + context.drawImage(getRecoloredImage(8, 0, 6), 0, 0); //EventUncoloredDetails + context.drawImage(getRecoloredImage(15, 0, 9), 0, 0); //EventBar + } + + //no Traveller + + context.textAlign = "center"; + context.textBaseline = "middle"; + //context.font = "small-caps" + context.font; + if (heirloomLine) + writeSingleLine(heirloomLine, 1074, 900, 1600, 58, "myText"); + if (isEachColorDark[0]) + context.fillStyle = "white"; + + if (document.getElementById("trait").checked) { + + if (typeLine) { + writeSingleLine(typeLine, 1075, 165, 780, 70); + } + + context.save(); + context.rotate(Math.PI * 3 / 2); + writeSingleLine(document.getElementById("title").value, -700, 2030, 750, 70); + context.restore(); + context.save(); + context.rotate(Math.PI / 2); + writeSingleLine(document.getElementById("title").value, 700, -120, 750, 70); + context.restore(); + + + } else { + + writeSingleLine(document.getElementById("title").value, 1075, 165, 780, 70); + + if (typeLine) { + context.save(); + context.translate(1903, 240); + context.rotate(45 * Math.PI / 180); + context.scale(1, 0.8); //yes, the letters are shorter + writeSingleLine(typeLine, 0, 0, 283, 64); + context.restore(); + } + + } + + if (priceLine) + writeLineWithIconsReplacedWithSpaces(priceLine + " ", 130, 205, 85 / 90, "mySpecials"); //adding a space confuses writeLineWithIconsReplacedWithSpaces into thinking this isn't a line that needs resizing + writeDescription("description", 1075, 1107, 1600, 283, 70); + writeIllustrationCredit(181, 1272, "black", "bold "); + writeCreatorCredit(1969, 1272, "black", "bold "); + + drawExpansionIcon(1930, 1190, 80, 80); + + } else if (templateSize == 2) { //double card + drawPicture(704, 1075, 1150, 564); + removeCorners(1403, 2151, 100); + + if (!recoloredImages[9]) recoloredImages[10] = false; + context.drawImage(getRecoloredImage(9, 0), 0, 0); //DoubleColorOne + if (!isEachColorDark[0]) + context.drawImage(images[3], 44, 1330, images[3].width, images[3].height * 2 / 3); //DescriptionFocus + context.save(); + context.rotate(Math.PI); + context.drawImage(getRecoloredImage(10, (normalColorCurrentIndices[1] > 0) ? 1 : 0), -1403, -2151); //DoubleColorOne again, but rotated + if (!isEachColorDark[1]) + context.drawImage(images[3], 44 - 1403, 1330 - 2151, images[3].width, images[3].height * 2 / 3); //DescriptionFocus + context.restore(); + context.drawImage(images[11], 0, 0); //DoubleUncoloredDetails //todo + + function drawHalfCard(t, l, p, d, colorID) { + context.textAlign = "center"; + context.textBaseline = "middle"; + //context.font = "small-caps" + context.font; + //writeSingleLine(document.getElementById(l).value, 701, 215, 1180, 75); + + var recolorFactors; + if (normalColorCurrentIndices[colorID] >= normalColorCustomIndices[colorID]) + recolorFactors = recolorFactorList[colorID]; + else + recolorFactors = normalColorFactorLists[normalColorCurrentIndices[colorID] - colorID][1]; + + context.save(); + var title = document.getElementById(l).value; + var size = 75 + 2; + do { + context.font = (size -= 2) + "pt myTitle"; + } while (context.measureText(title).width > 750); + context.textAlign = "left"; + context.fillStyle = "rgb(" + Math.round(recolorFactors[0] * 224) + "," + Math.round(recolorFactors[1] * 224) + "," + Math.round(recolorFactors[2] * 224) + ")"; + context.lineWidth = 15; + if (isEachColorDark[colorID]) + context.strokeStyle = "white"; + context.strokeText(title, 150, 1287); + context.fillText(title, 150, 1287); + context.restore(); + + if (isEachColorDark[colorID]) + context.fillStyle = "white"; + writeSingleLine(t, p ? 750 : 701, 1922, p ? 890 : 1190, 64); + if (p) + writeLineWithIconsReplacedWithSpaces(p + " ", 153, 1940, 85 / 90, "mySpecials"); + writeDescription(d, 701, 1600, 960, 460, 64); + context.restore(); + } + context.save(); + drawHalfCard(typeLine, "title", priceLine, "description", 0); + context.save(); + context.translate(1403, 2151); //bottom right corner + context.rotate(Math.PI); + shadowDistance = -shadowDistance; + drawHalfCard(heirloomLine, "title2", previewLine, "description2", (normalColorCurrentIndices[1] > 0) ? 1 : 0); + shadowDistance = -shadowDistance; + context.textAlign = "left"; + writeIllustrationCredit(150, 2038, "white", ""); + writeCreatorCredit(1253, 2038, "white", ""); + + drawExpansionIcon(1230, 1920, 80, 80); + + } else if (templateSize == 3) { //base card + drawPicture(704, 1075, 1150, 1898); + removeCorners(1403, 2151, 100); + + context.drawImage(getRecoloredImage(20, 0), 0, 0); //CardColorOne + context.drawImage(getRecoloredImage(21, 0, 6), 0, 0); //CardGray + context.drawImage(getRecoloredImage(22, 0, 9), 0, 0); //CardBrown + + context.textAlign = "center"; + context.textBaseline = "middle"; + //context.font = "small-caps" + context.font; + if (heirloomLine) { + context.drawImage(images[13], 97, 1720); //Heirloom banner + writeSingleLine(heirloomLine, 701, 1799, 1040, 58, "myText"); + } + if (isEachColorDark[1]) + context.fillStyle = "white"; + writeSingleLine(document.getElementById("title").value, 701, 215, previewLine ? 800 : 1180, 75); + if (typeLine.split(" - ").length >= 4) { + let types2 = typeLine.split(" - "); + let types1 = types2.splice(0, Math.ceil(types2.length / 2)); + writeSingleLine(types1.join(" - ") + " -", priceLine ? 750 : 701, 1945 - 26, priceLine ? 890 : 1180, 42); + writeSingleLine(types2.join(" - "), priceLine ? 750 : 701, 1945 + 26, priceLine ? 890 : 1180, 42); + } else { + if (expansion.height > 0 && expansion.width > 0) { + writeSingleLine(typeLine, priceLine ? 730 : 701, 1945, priceLine ? 800 : 900, 64); + } else { + writeSingleLine(typeLine, priceLine ? 750 : 701, 1945, priceLine ? 890 : 1180, 64); + } + } + if (priceLine) + writeLineWithIconsReplacedWithSpaces(priceLine + " ", 153, 1947, 85 / 90, "mySpecials"); //adding a space confuses writeLineWithIconsReplacedWithSpaces into thinking this isn't a line that needs resizing + if (previewLine) { + writeSingleLine(previewLine += " ", 223, 210, 0, 0, "mySpecials"); + writeSingleLine(previewLine, 1203, 210, 0, 0, "mySpecials"); + } + context.fillStyle = (isEachColorDark[0]) ? "white" : "black"; + if (!heirloomLine) + writeDescription("description", 701, 1060, 960, 1500, 64); + else + writeDescription("description", 701, 1000, 960, 1400, 64); + writeIllustrationCredit(165, 2045, "white", ""); + writeCreatorCredit(1225, 2045, "white", ""); + + drawExpansionIcon(1230, 1945, 80, 80); + } else if (templateSize == 4) { //pile marker + drawPicture(1075, 702, 1250, 870); + removeCorners(2151, 1403, 100); + + context.drawImage(getRecoloredImage(24, 0, 6), 0, 0); //CardGray + context.drawImage(getRecoloredImage(23, 0), 0, 0); //CardColorOne + + context.textAlign = "center"; + context.textBaseline = "middle"; + + context.save(); + if (isEachColorDark[1]) + context.fillStyle = "white"; + context.rotate(Math.PI / 2); + writeSingleLine(document.getElementById("title").value, 700, -1920, 500, 75); + context.restore(); + context.save(); + if (isEachColorDark[1]) + context.fillStyle = "white"; + context.rotate(Math.PI * 3 / 2); + writeSingleLine(document.getElementById("title").value, -700, 230, 500, 75); + context.restore(); + } else if (templateSize == 5) { //player mat + drawPicture(464, 342, 928, 684); + + + context.drawImage(getRecoloredImage(25, 0, 6), 0, 0); //MatBannerTop + if (document.getElementById("description").value.trim().length > 0) + context.drawImage(getRecoloredImage(26, 0, 6), 0, 0); //MatBannerBottom + + context.textAlign = "center"; + context.textBaseline = "middle"; + + if (isEachColorDark[1]) + context.fillStyle = "white"; + writeSingleLine(document.getElementById("title").value, 464, 96, 490, 55); + + writeDescription("description", 464, 572, 740, 80, 44); + + writeIllustrationCredit(15, 660, "white", "", 16); + writeCreatorCredit(913, 660, "white", "", 16); + + drawExpansionIcon(888, 40, 40, 40); + + } + + //finish up + //context.restore(); + + updateURL(); + + document.getElementById("load-indicator").setAttribute("style", "display:none;"); + canvases[0].parentNode.removeAttribute("data-status"); + return; + } + + // help function to load images CORS save // https://stackoverflow.com/a/43001137 + function loadImgAsBase64(url, callback, maxWidth, maxHeight) { + let canvas = document.createElement('CANVAS'); + let img = document.createElement('img'); + img.crossOrigin = "Anonymous"; + if (url.substr(0, 11) != 'data:image/' && url.substr(0, 8) != 'file:///') { + img.src = CORS_ANYWHERE_BASE_URL + url; + } else { + img.src = url; + } + img.onload = () => { + let context = canvas.getContext('2d'); + if (maxWidth > 0 && maxHeight > 0) { + canvas.width = maxWidth; + canvas.height = maxHeight; + } else { + canvas.height = img.height; + canvas.width = img.width; + } + context.drawImage(img, 0, 0, canvas.width, canvas.height); + let dataURL = canvas.toDataURL('image/png'); + canvas = null; + callback(dataURL); + }; + img.onerror = () => { + useCORS = false; + console.log("CORS loading of external resources deactivated"); + callback(url); + }; + } + + + // initialize stage + var sources = [ + "CardColorOne.png", + "CardColorTwo.png", + "CardGray.png", + "DescriptionFocus.png", + "Traveller.png", + "", //illustration //5 + "EventColorOne.png", + "EventColorTwo.png", + "EventBrown.png", + "DoubleColorOne.png", + "DoubleColorOne.png", //10 + "DoubleUncoloredDetails.png", + "CardColorTwoNight.png", + "Heirloom.png", + "EventHeirloom.png", + "EventBrown2.png", //15 + "CardBrown.png", + "", //expansion + "CardColorTwoSmall.png", + "CardColorTwoBig.png", + "BaseCardColorOne.png", //20 + "BaseCardGray.png", + "BaseCardBrown.png", + "PileMarkerColorOne.png", + "PileMarkerGrey.png", + "MatBannerTop.png", //25 + "MatBannerBottom.png", + "CardColorThree.png", + "TraitColorOne.png", + "TraitBrown.png", + "TraitColorOneSide.png", //30 + "TraitBrownSide.png" + //icons come afterwards + ]; + for (var i = 0; i < sources.length; i++) + recoloredImages.push(false); + var legend = document.getElementById("legend"); + var numberFirstIcon = sources.length; + for (key in icons) { + var li = document.createElement("li"); + li.textContent = ": " + icons[key][0]; + var span = document.createElement("span"); + span.classList.add("def"); + span.textContent = key.replace("\\", ""); + li.insertBefore(span, li.firstChild); + legend.insertBefore(li, legend.firstChild); + sources.push(icons[key][0] + ".png"); + } + for (var i = 0; i < sources.length; i++) { + images.push(new Image()); + images[i].crossOrigin = "Anonymous"; + images[i].src = "card-resources/" + sources[i]; + } + + var simpleOnChangeInputCheckboxIDs = ["traveller", "trait"]; + var simpleOnChangeInputFieldIDs = ["title", "description", "type", "credit", "creator", "price", "preview", "type2", "color2split", "boldkeys", "picture-x", "picture-y", "picture-zoom"]; + simpleOnChangeInputFieldIDs = simpleOnChangeInputFieldIDs.concat(simpleOnChangeInputCheckboxIDs); + var simpleOnChangeButOnlyForSize2InputFieldIDs = ["title2", "description2"]; + for (var i = 0; i < simpleOnChangeInputFieldIDs.length; ++i) { + document.getElementById(simpleOnChangeInputFieldIDs[i]).onchange = queueDraw; + if (i < simpleOnChangeButOnlyForSize2InputFieldIDs.length) + document.getElementById(simpleOnChangeButOnlyForSize2InputFieldIDs[i]).onchange = queueDraw; + } + var recolorInputs = document.getElementsByName("recolor"); + var alreadyNeededToDetermineCustomAccentColors = false; + for (var i = 0; i < recolorInputs.length; ++i) + recolorInputs[i].onchange = function (i) { + return function () { + var val = parseFloat(this.value); + if (val !== NaN) { + var imageID = Math.floor(i / 12); + if (normalColorCurrentIndices[imageID] >= 10) { //potentially recoloring the supposedly Uncolored images + recoloredImages[2] = false; + recoloredImages[8] = false; + recoloredImages[11] = false; + recoloredImages[15] = false; + recoloredImages[16] = false; + recoloredImages[29] = false; + recoloredImages[31] = false; + } + recoloredImages[imageID] = false; + recoloredImages[imageID + 6] = false; + recoloredImages[imageID + 9] = false; + recoloredImages[12] = false; + recoloredImages[18] = false; + recoloredImages[19] = false; + recoloredImages[20] = false; + recoloredImages[23] = false; + recoloredImages[28] = false; + recoloredImages[30] = false; + recolorFactorList[imageID][i % 12] = val; + queueDraw(); + } + } + }(i); + + function setImageSource(id, src) { + images[id].src = src; + images[id].crossOrigin = "Anonymous"; + imagesLoaded = false; + queueDraw(250); + } + + function onChangeExternalImage(id, value, maxWidth, maxHeight) { + let url = (sources[id] = value.trim()); + + if (url != "[local image]") { + if (url.length > 0 && useCORS) { + loadImgAsBase64(url, (dataURL) => { + setImageSource(id, dataURL) + }, maxWidth, maxHeight); + } else { + setImageSource(id, url); + } + } + } + + function onUploadImage(id, file) { + var reader = new FileReader(); + reader.onload = () => { + setImageSource(id, reader.result); + console.log("image loaded"); + }; + reader.readAsDataURL(file); + } + + + if (document.getElementById("trait").checked) { + document.body.classList.add("trait"); + } + + document.getElementById("trait").addEventListener('change', () => { + if (document.getElementById("trait").checked) { + document.body.classList.add("trait"); + } else { + document.body.classList.remove("trait"); + } + }, false); + + try { + // Image 5 = Main Picture + document.getElementById("picture").onchange = function () { + document.getElementById("picture-upload").value = ""; + onChangeExternalImage(5, this.value); + }; + document.getElementById("picture-upload").onchange = (event) => { + document.getElementById("picture").value = "[local image]"; + onUploadImage(5, event.target.files[0]); + }; + } catch (err) {} + + try { + // Image 17 = Expansion Icon + document.getElementById("expansion").onchange = function () { + document.getElementById("expansion-upload").value = ""; + onChangeExternalImage(17, this.value); + }; + document.getElementById("expansion-upload").onchange = (event) => { + document.getElementById("expansion").value = "[local image]"; + onUploadImage(17, event.target.files[0]); + }; + } catch (err) {} + + try { + //Last Icon = Custom Icon + var customIcon = document.getElementById("custom-icon"); + onChangeExternalImage(images.length - 1, customIcon.value, 156, 156); + customIcon.onchange = function () { + document.getElementById("custom-icon-upload").value = ""; + onChangeExternalImage(images.length - 1, this.value, 156, 156); + }; + document.getElementById("custom-icon-upload").onchange = (event) => { + customIcon.value = "[local image]"; + onUploadImage(images.length - 1, event.target.files[0]); + }; + } catch (err) {} + + var genericCustomAccentColors = [ + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1.2, 0.8, 0.5], + [0, 0, 0, 0, 0, 0, 0.9, 0.8, 0.7, 0.9, 0.8, 0.7] + ]; + for (i = 0; i < normalColorDropdowns.length; ++i) + normalColorDropdowns[i].onchange = function (i) { + return function () { + if (normalColorCurrentIndices[i] >= 10 || this.selectedIndex >= 10) { //potentially recoloring the supposedly Uncolored images + recoloredImages[2] = false; + recoloredImages[8] = false; + recoloredImages[11] = false; + recoloredImages[15] = false; + recoloredImages[16] = false; + recoloredImages[29] = false; + recoloredImages[31] = false; + } + normalColorCurrentIndices[i] = this.selectedIndex; + recoloredImages[i] = false; + recoloredImages[i + 6] = false; + recoloredImages[i + 9] = false; + recoloredImages[2] = false; + recoloredImages[12] = false; + recoloredImages[18] = false; + recoloredImages[19] = false; + recoloredImages[20] = false; + recoloredImages[23] = false; + recoloredImages[28] = false; + recoloredImages[30] = false; + var delta = normalColorCustomIndices[i] - this.selectedIndex; + if (delta <= 0) + this.nextElementSibling.removeAttribute("style"); + else + this.nextElementSibling.setAttribute("style", "display:none;"); + if (delta === -1) { + this.nextElementSibling.nextElementSibling.removeAttribute("style"); + if (i === 0 && !alreadyNeededToDetermineCustomAccentColors) { + alreadyNeededToDetermineCustomAccentColors = true; + for (var j = 6; j < 12; ++j) + recolorFactorList[0][j] = recolorInputs[j].value = genericCustomAccentColors[templateSize & 1][j]; + } + } else + this.nextElementSibling.nextElementSibling.setAttribute("style", "display:none;"); + queueDraw(1); + } + }(i); + var templateSizeInputs = document.getElementsByName("size"); + for (var i = 0; i < templateSizeInputs.length; ++i) + templateSizeInputs[i].onchange = function (i) { + return function () { + templateSize = parseInt(this.value); + document.body.className = this.id; + document.body.classList.add("trait"); + document.getElementById("load-indicator").removeAttribute("style"); + queueDraw(250); + } + }(i); + + //ready to begin: load information from query parameters + var query = getQueryParams(document.location.search); + document.body.className = ""; + for (var queryKey in query) { + switch (queryKey) { + case "color0": + normalColorCurrentIndices[0] = normalColorDropdowns[0].selectedIndex = query[queryKey]; + break; + case "color1": + normalColorCurrentIndices[1] = normalColorDropdowns[1].selectedIndex = query[queryKey]; + break; + case "size": + var buttonElement = document.getElementsByName("size")[templateSize = parseInt(query[queryKey])]; + document.body.classList.add(buttonElement.id); + buttonElement.checked = true; + break; + case "traveller": + var checkboxElement = document.getElementById(queryKey); + checkboxElement.checked = query[queryKey] === 'true'; + break; + case "trait": + var checkboxElement = document.getElementById(queryKey); + checkboxElement.checked = query[queryKey] === 'true'; + if (checkboxElement.checked === true) { + document.body.classList.add(queryKey); + } + break; + default: + var matches = queryKey.match(/^c(\d)\.(\d)$/); + if (matches) { + var id = matches[1]; + normalColorCurrentIndices[id] = normalColorDropdowns[id].selectedIndex = normalColorCustomIndices[id]; + normalColorDropdowns[id].nextElementSibling.removeAttribute("style"); + recolorFactorList[id][matches[2]] = recolorInputs[12 * id + parseInt(matches[2])].value = parseFloat(query[queryKey]); + } else { + matches = queryKey.match(/^c(\d)\.(\d)\.(\d)$/); + if (matches) { + alreadyNeededToDetermineCustomAccentColors = true; + var id = matches[1]; + normalColorCurrentIndices[id] = normalColorDropdowns[id].selectedIndex = normalColorCustomIndices[id] + 1; + normalColorDropdowns[id].nextElementSibling.removeAttribute("style"); + normalColorDropdowns[id].nextElementSibling.nextElementSibling.removeAttribute("style"); + recolorFactorList[id][parseInt(matches[2]) * 3 + parseInt(matches[3])] = recolorInputs[12 * id + 3 * parseInt(matches[2]) + parseInt(matches[3])].value = parseFloat(query[queryKey]); + } else { + var el = document.getElementById(queryKey); + if (el) + el.value = query[queryKey]; + } + } + break; + } + for (var i = 0; i < simpleOnChangeButOnlyForSize2InputFieldIDs.length; ++i) + if (!document.getElementById(simpleOnChangeButOnlyForSize2InputFieldIDs[i]).value) + document.getElementById(simpleOnChangeButOnlyForSize2InputFieldIDs[i]).value = document.getElementById(simpleOnChangeButOnlyForSize2InputFieldIDs[i].substr(0, simpleOnChangeButOnlyForSize2InputFieldIDs[i].length - 1)).value; + } + //set the illustration's Source properly and also call queueDraw. + document.getElementById("picture").onchange(); + document.getElementById("expansion").onchange(); + document.getElementById("custom-icon").onchange(); + + //adjust page title + function adjustPageTitle() { + let cardTitle = document.getElementById("title").value.trim(); + let creator = document.getElementById("creator").value.trim(); + let pageDefaultTitle = "Dominion Card Image Generator"; + document.title = cardTitle.length > 0 ? (pageDefaultTitle + " - " + cardTitle + " " + creator) : pageDefaultTitle; + }; + document.getElementById('title').addEventListener('change', adjustPageTitle, false); + document.getElementById('creator').addEventListener('change', adjustPageTitle, false); + adjustPageTitle(); + + //redraw after color switch + document.getElementById('color-switch-button').addEventListener('click', switchColors, false); + + //pass parameters to original version to enable easy comparison + document.getElementById('linkToOriginal').addEventListener('click', function (event) { + event.preventDefault(); + window.location.href = this.href + document.location.search; + }, false); + +} diff --git a/root.ts b/root.ts new file mode 100644 index 0000000..a11a402 --- /dev/null +++ b/root.ts @@ -0,0 +1,3 @@ +import { dirname, fromFileUrl } from "jsr:@std/path"; + +export const projectRootDir = dirname(fromFileUrl(import.meta.url)); diff --git a/src/cards.ts b/src/cards.ts new file mode 100644 index 0000000..628db64 --- /dev/null +++ b/src/cards.ts @@ -0,0 +1,304 @@ +import { + DominionCard, + TYPE_ACTION, + TYPE_DURATION, + TYPE_NIGHT, + TYPE_TREASURE, + TYPE_VICTORY, +} from "./types.ts"; + +const expansionIcon = ""; +const author = "Dylan"; + +const _sampleCard = { + orientation: "card", + title: "Sample", + description: "", + types: [TYPE_ACTION], + image: "", + artist: "", + author, + version: "0.1", + cost: "$", + preview: "", + expansionIcon, +}; + +export const cards: DominionCard[] = [ + { + orientation: "card", + title: "Flask", + description: + "+2 Cards\n\nAt the start of your Clean-up phase, you may put a card from your hand onto your deck.", + types: [TYPE_TREASURE], + image: "", + artist: "", + author, + version: "0.1", + cost: "$6", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Promising Land", + description: "Worth 1% per 4 cards you have that cost $4 or $5.", + types: [TYPE_VICTORY], + image: "", + artist: "", + author, + version: "0.1", + cost: "$4", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Steelworker", + description: + "If it's your Action phase, +3 Cards.\n\nIf it's your Buy phase, +1 Buy, and +$1.", + types: [TYPE_ACTION, TYPE_TREASURE], + image: "", + artist: "", + author, + version: "0.2", + cost: "$5", + preview: "$?", + expansionIcon, + }, + { + orientation: "card", + title: "Shovel", + description: + "Play a Treasure card from your hand. Then trash it from play to gain a Treasure card costing up to $3 more than it.", + types: [TYPE_TREASURE], + image: "", + artist: "", + author, + version: "0.1", + cost: "$6", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "High Council", + description: + "+2 Cards\n+1 Action\n+1 Buy\n\nEach player (including you) may choose one: +1 Card, or trash a card from their hand.", + types: [TYPE_ACTION], + image: "", + artist: "", + author: "Lou + Dylan", + version: "0.1", + cost: "$7", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Productive Village", + description: + "If it's your Action phase, +3 Actions.\n\nIf it's your Buy phase, +$1 per unused Action you have (Action, not Action card). +$1 if you have no Actions.", + types: [TYPE_ACTION, TYPE_TREASURE], + image: "", + artist: "", + author: "Dylan", + version: "0.2", + cost: "$3", + preview: "$?", + expansionIcon, + }, + { + orientation: "card", + title: "Secret Society", + description: + "+1 Action\n\nIf you have at least 3 copies of Secret Society in play, trash all of them to gain any number of cards costing at least $2, whose total combined cost is at most $50.\n\n-\n\nThis card cannot be gained other than by buying it. During a player's buy phase, this costs $3 plus $2 per Secret Society they've gained this game.", + types: [TYPE_ACTION], + image: "", + artist: "", + author: "Dylan", + version: "0.1", + cost: "$4*", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Eclipse", + description: + "+1 Card\n\nIf you have no Actions, +1 Action. If you have no Buys, +1 Buy. Return to your Action phase.", + types: [TYPE_NIGHT], + image: "", + artist: "", + author: "Dylan", + version: "0.1", + cost: "$5", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Moonlit Scheme", + description: "You may play an Action card from your hand.", + types: [TYPE_NIGHT], + image: "", + artist: "", + author: "Dylan", + version: "0.1", + cost: "$2", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Beaver", + description: + "Pay $1. If you did, gain a card costing up to the amount of $ you have.", + types: [TYPE_NIGHT], + image: "", + artist: "", + author: "Dylan", + version: "0.1", + cost: "$3", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Silk", + description: "Choose one: +$2, or gain a Silver.", + types: [TYPE_TREASURE], + image: "", + artist: "", + author, + version: "0.1", + cost: "$4", + preview: "$?", + expansionIcon, + }, + { + orientation: "card", + title: "Foundry", + description: + "Choose one: +1 Card, +1 Action and +$1; or trash a card from your hand to gain a card that costs up to $2 more than it.", + types: [TYPE_ACTION], + image: "", + artist: "", + author, + version: "0.1", + cost: "$5", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Vendor", + description: + "Choose three different options: +1 Card, +1 Action, +1 Buy, +$1, trash a card from your hand.", + types: [TYPE_ACTION], + image: "", + artist: "", + author, + version: "0.2", + cost: "$5", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Chateau", + description: + "1%\n\n-\n\nWhen you gain this, choose one: gain an Estate; or +1 Card, +1 Action, +1 Buy, +$1, and if it's your Buy phase, return to your Action phase.", + types: [TYPE_VICTORY], + image: "", + artist: "", + author, + version: "0.1", + cost: "$3", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Retainer", + description: + "Set aside a card from your hand (under this).\n\nAt any time during any of your turns, you may take +1 Action, and add the set aside card to your hand, discarding this from play.\n\nAt the start of each of your Buy phases, if the card is still set aside, +@1.", + types: [TYPE_ACTION, TYPE_DURATION], + image: "", + artist: "", + author, + version: "0.2", + cost: "$2", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Crop Field", + description: "$1\n\n-\n\n1%", + types: [TYPE_TREASURE, TYPE_VICTORY], + image: "", + artist: "", + author, + version: "0.1", + cost: "$3", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Duet", + description: + "Play one of the set aside cards, leaving it there\n\n-\n\nSetup: set aside two unused non-Duration Action cards of the same cost. This costs $1 more than the cost of the set aside cards.", + types: [TYPE_ACTION], + image: "", + artist: "", + author, + version: "0.1", + cost: "$?", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Scraps", + description: + "If it's your Action phase, trash up to 3 cards from your hand.\n\nIf it's your Buy phase, +$1 per 10 cards in the trash (round down).", + types: [TYPE_ACTION, TYPE_TREASURE], + image: "", + artist: "", + author, + version: "0.1", + cost: "$4", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Slogger", + description: + "+1 Card\n+1 Action\n\nReveal the top 5 cards of your deck. Put the Victory cards into your hand, and put the rest back on top in any order.", + types: [TYPE_ACTION], + image: "", + artist: "", + author, + version: "0.1", + cost: "$4", + preview: "", + expansionIcon, + }, + { + orientation: "card", + title: "Vase", + description: + "$2\nReturn this to its pile.\n\n-\n\nWhen you gain this, gain another Vase (that doesn't come with another).", + types: [TYPE_TREASURE], + image: "", + artist: "", + author, + version: "0.1", + cost: "$3", + preview: "", + expansionIcon, + }, +]; diff --git a/src/client/App.tsx b/src/client/App.tsx new file mode 100644 index 0000000..91de6a7 --- /dev/null +++ b/src/client/App.tsx @@ -0,0 +1,10 @@ +import { cards } from "../cards.ts"; +import { Card } from "./Card.tsx"; + +export const App = () => { + return
+ {cards.map((card) => { + return + })} +
; +}; diff --git a/src/client/Card.tsx b/src/client/Card.tsx new file mode 100644 index 0000000..2b1ab21 --- /dev/null +++ b/src/client/Card.tsx @@ -0,0 +1,28 @@ +import { drawCard, loadImages, loadFonts } from "../draw.ts"; +import { DominionCard } from "../types.ts"; + +const sizeMap = { + card: { + width: 1403, + height: 2151, + }, + landscape: { + width: 2151, + height: 1403, + } +} + +export const Card = (props: {card: DominionCard}) => { + const {card} = props; + const {width, height} = sizeMap[card.orientation]; + return { + if (canvasElement) { + const context = canvasElement.getContext("2d"); + if (context) { + await loadFonts(); + await loadImages(); + await drawCard(context, card); + } + } + }}> +} \ No newline at end of file diff --git a/src/client/index.tsx b/src/client/index.tsx new file mode 100644 index 0000000..af7cc6b --- /dev/null +++ b/src/client/index.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom"; +import { App } from "./App.tsx"; + +const rootElement = document.getElementById("root"); +if (!rootElement) { + throw Error("No root element to attach react to."); +} + +createRoot(rootElement).render( + + + , +); diff --git a/src/colorhelper.ts b/src/colorhelper.ts new file mode 100644 index 0000000..7ff915a --- /dev/null +++ b/src/colorhelper.ts @@ -0,0 +1,5 @@ +import parseColor1 from "npm:parse-color"; + +export const parseColor = (c: string): { rgb: [number, number, number] } => { + return parseColor1(c); +}; diff --git a/src/dominiontext.ts b/src/dominiontext.ts new file mode 100644 index 0000000..083ee21 --- /dev/null +++ b/src/dominiontext.ts @@ -0,0 +1,480 @@ +import { getImage } from "./draw.ts"; +import { parseFont, stringifyFont } from "./fonthelper.ts"; + +export type Piece = + | { type: "text"; text: string; isBold?: boolean; isItalic?: boolean } + | { type: "space" } + | { type: "break" } + | { type: "hr" } + | { + type: "symbol"; + symbol: "coin" | "debt" | "potion" | "vp" | "vp-token" | "sun"; + isBig?: boolean; + prefix?: string; + text: string; + textColor: string; + }; + +type PromiseOr = T | Promise; + +type PieceMeasure = { + type: "content" | "space" | "break"; + width: number; + ascent: number; + descent: number; +}; + +type Line = { + pieces: { + piece: Piece; + measure: PieceMeasure; + xOffset: number; + }[]; + width: number; + ascent: number; + descent: number; +}; + +type PieceTools = { + measurePiece: ( + context: CanvasRenderingContext2D, + piece: Piece + ) => PromiseOr; + renderPiece: ( + context: CanvasRenderingContext2D, + piece: Piece, + x: number, + y: number + ) => PromiseOr; +}; + +type PieceDef = { + type: T; + measure( + context: CanvasRenderingContext2D, + piece: Piece & { type: T }, + tools: PieceTools + ): PromiseOr; + render( + context: CanvasRenderingContext2D, + piece: Piece & { type: T }, + x: number, + y: number, + measure: NoInfer, + tools: PieceTools + ): PromiseOr; +}; + +const pieceDef = ( + def: PieceDef +) => { + return def; +}; + +const textPiece = pieceDef({ + type: "text", + measure(context, piece) { + context.save(); + const fontInfo = parseFont(context.font); + if (piece.isBold) { + fontInfo.weight = "bold"; + } + if (piece.isItalic) { + fontInfo.style = "italic"; + } + const font = stringifyFont(fontInfo); + context.font = font; + const metrics = context.measureText(piece.text); + context.restore(); + return { + type: "content", + width: metrics.width, + ascent: metrics.fontBoundingBoxAscent, + descent: metrics.fontBoundingBoxDescent, + font, + }; + }, + render(context, piece, x, y, measure) { + context.save(); + context.font = measure.font; + context.fillText(piece.text, x, y); + context.restore(); + }, +}); + +const spacePiece = pieceDef({ + type: "space", + measure(context, _piece) { + const metrics = context.measureText(" "); + return { + type: "space", + width: metrics.width, + ascent: metrics.fontBoundingBoxAscent, + descent: metrics.fontBoundingBoxDescent, + }; + }, + render() {}, +}); + +const breakPiece = pieceDef({ + type: "break", + measure(context, _piece) { + const metrics = context.measureText(" "); + return { + type: "break", + width: 0, + ascent: metrics.fontBoundingBoxAscent / 3, + descent: metrics.fontBoundingBoxDescent / 3, + }; + }, + render() {}, +}); + +const hrPiece = pieceDef({ + type: "hr", + measure(context, _piece) { + const metrics = context.measureText(" "); + const h = + (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) / + 3; + return { + type: "content", + width: 750, + ascent: h / 2, + descent: h / 2, + }; + }, + render(context, _piece, x, y, measure) { + context.save(); + context.beginPath(); + context.moveTo(x, y); + context.lineTo(x + measure.width, y); + context.lineWidth = 8; + context.stroke(); + context.restore(); + }, +}); + +const symbolPiece = pieceDef({ + type: "symbol", + measure(context, piece) { + context.save(); + const metrics = context.measureText(" "); + const height = + metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; + const prefixMetrics = context.measureText(piece.prefix ?? ""); + const coinImage = getImage(piece.symbol); + context.restore(); + const { isBig } = piece; + const scale = isBig ? 2.5 : 1; + return { + type: "content", + width: + scale * + (prefixMetrics.width + + coinImage.width * (height / coinImage.height)), + ascent: scale * metrics.fontBoundingBoxAscent, + descent: scale * metrics.fontBoundingBoxDescent, + prefixWidth: scale * prefixMetrics.width, + scale, + }; + }, + render(context, piece, x, y, measure) { + if (piece.isBig) { + console.log("big", piece, measure); + } + context.save(); + // context.fillStyle = "yellow"; + const height = measure.ascent + measure.descent; + // context.fillRect(x, y - measure.ascent, measure.width, height); + context.drawImage( + getImage(piece.symbol), + x + measure.prefixWidth, + y - measure.ascent, + measure.width - measure.prefixWidth, + height + ); + + context.save(); + const prefixFontInfo = parseFont(context.font); + prefixFontInfo.weight = "bold"; + prefixFontInfo.size = + parseInt(prefixFontInfo.size.toString()) * measure.scale; + const prefixFont = stringifyFont(prefixFontInfo); + context.font = prefixFont; + context.fillText(piece.prefix ?? "", x, y); + context.restore(); + + const fontInfo = parseFont(context.font); + fontInfo.family = ["DominionSpecial"]; + fontInfo.weight = "bold"; + fontInfo.size = + parseInt(fontInfo.size.toString()) * 1.2 * measure.scale; + const font = stringifyFont(fontInfo); + context.font = font; + context.fillStyle = piece.textColor; + context.textAlign = "center"; + context.fillText( + piece.text, + x + measure.prefixWidth + (measure.width - measure.prefixWidth) / 2, + y + ); + context.restore(); + }, +}); + +const pieceDefs = [textPiece, spacePiece, breakPiece, symbolPiece, hrPiece]; + +// deno-lint-ignore no-explicit-any +const tools: PieceTools = {} as any; + +const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => { + const def = pieceDefs.find((def) => def.type === piece.type)!; + // deno-lint-ignore no-explicit-any + return def.measure(context, piece as any, tools); +}; + +const renderPiece = ( + context: CanvasRenderingContext2D, + piece: Piece, + x: number, + y: number +) => { + const def = pieceDefs.find((def) => def.type === piece.type)!; + // deno-lint-ignore no-explicit-any + const measure = def.measure(context, piece as any, tools); + // deno-lint-ignore no-explicit-any + return def.render(context, piece as any, x, y, measure as any, tools); +}; + +tools.measurePiece = measurePiece; +tools.renderPiece = renderPiece; + +type DominionFont = { + font: "text" | "title"; + size: number; + isBold: boolean; + isItalic: boolean; +}; + +type PieceWithInfo = { + piece: Piece; + measure: PieceMeasure; +}; + +export const measureDominionText = async ( + context: CanvasRenderingContext2D, + pieces: Piece[], + maxWidth = Infinity +) => { + const data: PieceWithInfo[] = await Promise.all( + pieces.map(async (piece) => ({ + piece, + measure: await measurePiece(context, piece), + })) + ); + let lines: Line[] = [{ pieces: [], width: 0, ascent: 0, descent: 0 }]; + for (const pieceInfo of data) { + const line = lines[lines.length - 1]!; + if (pieceInfo.measure.type === "break") { + line.pieces.push({ ...pieceInfo, xOffset: line.width }); + line.width += pieceInfo.measure.width; + line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent); + line.descent = Math.max(line.descent, pieceInfo.measure.descent); + lines.push({ pieces: [], width: 0, ascent: 0, descent: 0 }); + } else { + if (line.width + pieceInfo.measure.width > maxWidth) { + lines.push({ + pieces: [{ ...pieceInfo, xOffset: 0 }], + width: pieceInfo.measure.width, + ascent: pieceInfo.measure.ascent, + descent: pieceInfo.measure.descent, + }); + } else { + line.pieces.push({ + ...pieceInfo, + xOffset: line.width, + }); + line.width += pieceInfo.measure.width; + line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent); + line.descent = Math.max( + line.descent, + pieceInfo.measure.descent + ); + } + } + } + lines = lines.map((line) => { + while ( + line.pieces[line.pieces.length - 1] && + line.pieces[line.pieces.length - 1]!.measure.type === "space" + ) { + line.pieces = line.pieces.slice(0, -1); + } + line.width = line.pieces + .map((piece) => piece.measure.width) + .reduce((a, b) => a + b, 0); + return line; + }); + return { + lines, + width: Math.max(...lines.map((line) => line.width)), + height: lines + .map((line) => line.ascent + line.descent) + .reduce((a, b) => a + b, 0), + }; +}; + +const debug = false; + +export const renderDominionText = async ( + context: CanvasRenderingContext2D, + pieces: Piece[], + x: number, + y: number, + maxWidth = Infinity +) => { + const { lines, height } = await measureDominionText( + context, + pieces, + maxWidth + ); + let yOffset = 0; + for (const line of lines) { + yOffset += line.ascent; + for (const { piece, xOffset } of line.pieces) { + await renderPiece( + context, + piece, + x - line.width / 2 + xOffset, + y - height / 2 + yOffset + ); + if (debug) { + context.save(); + context.strokeStyle = "blue"; + context.lineWidth = 5; + const pieceMeasure = await measurePiece(context, piece); + context.strokeRect( + x - line.width / 2 + xOffset, + y - height / 2 - line.ascent + yOffset, + pieceMeasure.width, + pieceMeasure.ascent + pieceMeasure.descent + ); + context.strokeStyle = "red"; + context.beginPath(); + context.moveTo( + x - line.width / 2 + xOffset - 5, + y - height / 2 + yOffset + ); + context.lineTo( + x - line.width / 2 + xOffset + 5, + y - height / 2 + yOffset + ); + context.stroke(); + context.restore(); + } + } + yOffset += line.descent; + } +}; + +export const parse = ( + text: string, + options?: { isDescription: boolean } +): Piece[] => { + const { isDescription = false } = options ?? {}; + const pieces: Piece[] = []; + const symbolMap = { + "$": { symbol: "coin", textColor: "black" }, + "@": { symbol: "debt", textColor: "white" }, + "^": { symbol: "potion", textColor: "white" }, + "%": { symbol: "vp", textColor: "white" }, + "#": { symbol: "vp-token", textColor: "black" }, + "*": { symbol: "sun", textColor: "black" }, + } as const; + for (let i = 0; i < text.length; i++) { + const char = text[i]!; + if (char === " ") { + pieces.push({ type: "space" }); + } else if (char === "\n") { + pieces.push({ type: "break" }); + } else if (char in symbolMap) { + const c = char as keyof typeof symbolMap; + const end = text.slice(i).match(new RegExp(`\\${c}[^ \n.,;]*`))![0] + .length; + const isBig = + isDescription && + ["\n", undefined].includes(text[i - 1]) && + ["\n", undefined].includes(text[i + end]); + pieces.push({ + type: "symbol", + ...symbolMap[c], + text: text.slice(i + 1, i + end), + isBig, + }); + i += end - 1; + } else if (char === "+") { + const match = text.slice(i).match(/\+\d*( \w+)?/); + if (match) { + const end = match[0].length; + pieces.push({ + type: "text", + isBold: true, + text: text.slice(i, i + end), + }); + i += end - 1; + } else { + pieces.push({ + type: "text", + isBold: true, + text: "+", + }); + } + } else if ( + char === "-" && + text[i - 1] === "\n" && + text[i + 1] === "\n" + ) { + pieces.push({ type: "hr" }); + } else if (/\d/.test(char)) { + const match = text.slice(i).match( + new RegExp( + `\\d+(${Object.keys(symbolMap) + .map((s) => `\\${s}`) + .join("|")})` + ) + ); + if (match) { + const end = match[0].length; + const symbolChar = match[1] as keyof typeof symbolMap; + const isBig = + isDescription && + ["\n", undefined].includes(text[i - 1]) && + ["\n", undefined].includes(text[i + end]); + pieces.push({ + type: "symbol", + ...symbolMap[symbolChar], + prefix: text.slice(i, i + end - 1), + text: "", + isBig, + }); + i += end - 1; + } else { + const end = text.slice(i).match(/\d+/)![0].length; + pieces.push({ type: "text", text: text.slice(i, i + end) }); + i += end - 1; + } + } else { + const end = text.slice(i).match( + new RegExp( + `[^${Object.keys(symbolMap) + .map((s) => `\\${s}`) + .join("")} \n]+` + ) + )![0].length; + pieces.push({ type: "text", text: text.slice(i, i + end) }); + i += end - 1; + } + } + return pieces; +}; diff --git a/src/draw.ts b/src/draw.ts new file mode 100644 index 0000000..b0978df --- /dev/null +++ b/src/draw.ts @@ -0,0 +1,405 @@ +import { parseColor } from "./colorhelper.ts"; +import { + measureDominionText, + parse, + renderDominionText, +} from "./dominiontext.ts"; +import { DominionCardType, TYPE_ACTION } from "./types.ts"; +import { DominionCard } from "./types.ts"; + +const imageCache: Record = {}; +export const loadImage = ( + src: string, + key?: string +): Promise => { + return new Promise((resolve) => { + if (key && key in imageCache && imageCache[key]) { + resolve(imageCache[key]); + } + const img = new Image(); + img.onload = () => { + if (key) { + imageCache[key] = img; + } + resolve(img); + }; + img.onerror = (e) => { + console.log("err", e); + resolve(null); + }; + img.src = src; + }); +}; + +const imageList = [ + { + key: "card-color-1", + src: "/static/assets/CardColorOne.png", + }, + { + key: "card-color-2", + src: "/static/assets/CardColorTwo.png", + }, + { + key: "card-color-2-night", + src: "/static/assets/CardColorTwoNight.png", + }, + { + key: "card-brown", + src: "/static/assets/CardBrown.png", + }, + { + key: "card-gray", + src: "/static/assets/CardGray.png", + }, + { + key: "card-description-focus", + src: "/static/assets/DescriptionFocus.png", + }, + { + key: "coin", + src: "/static/assets/Coin.png", + }, + { + key: "debt", + src: "/static/assets/Debt.png", + }, + { + key: "potion", + src: "/static/assets/Potion.png", + }, + { + key: "vp", + src: "/static/assets/VP.png", + }, + { + key: "vp-token", + src: "/static/assets/VP-Token.png", + }, + { + key: "sun", + src: "/static/assets/Sun.png", + }, +]; + +export const loadImages = async () => { + for (const imageInfo of imageList) { + const { key, src } = imageInfo; + await loadImage(src, key); + } +}; + +export const getImage = (key: string) => { + const image = imageCache[key]; + if (!image) { + throw Error(`Tried to get an invalid image ${key}`); + } + return image; +}; + +export const loadFonts = async () => { + const titleFont = new FontFace( + "DominionTitle", + `local("Trajan Pro Bold"), local("TrajanPro-Bold"), local('Trajan Pro'), + url('https://fonts.cdnfonts.com/s/14928/TrajanPro-Bold.woff') format('woff'), + url('https://shemitz.net/static/dominion3/Trajan%20Pro%20Bold.ttf') format('truetype'), + url('https://dominion.games/fonts/TrajanPro-Bold.otf') format('opentype'), + local("Trajan"), + local("Optimus Princeps"), + url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2')` + ); + + const specialFont = new FontFace( + "DominionSpecial", + `local("Minion Std Black"), local("MinionStd-Black"), local("Minion Std"), local('Minion Pro'), + url('https://fonts.cdnfonts.com/s/13260/MinionPro-Regular.woff') format('woff'), + url('https://shemitz.net/static/dominion3/MinionStd-Black.otf') format('opentype'), + local("Optimus Princeps"), + url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2')` + ); + + // deno-lint-ignore no-explicit-any + (document.fonts as any).add(titleFont); + // deno-lint-ignore no-explicit-any + (document.fonts as any).add(specialFont); + + await Promise.all([titleFont.load(), specialFont.load()]); +}; + +export const colorImage = ( + image: HTMLImageElement, + color?: string +): HTMLCanvasElement => { + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + const context = canvas.getContext("2d")!; + context.save(); + context.drawImage(image, 0, 0); + context.globalCompositeOperation = "multiply"; + context.fillStyle = color ?? "white"; + context.fillRect(0, 0, canvas.width, canvas.height); + context.globalCompositeOperation = "destination-atop"; // restore transparency + context.drawImage(image, 0, 0); + context.restore(); + return canvas; +}; + +export const drawCard = ( + context: CanvasRenderingContext2D, + card: DominionCard +): Promise => { + if (card.orientation === "card") { + return drawStandardCard(context, card); + } else { + return drawLandscapeCard(context, card); + } +}; + +const _rgbCache: Record = {}; +const getColorRgb = (c: string): { r: number; g: number; b: number } => { + const { rgb } = parseColor(c); + const [r, g, b] = rgb; + return { r, g, b }; + // if (c in _rgbCache) { + // return _rgbCache[c]!; + // } + // const canvas = document.createElement("canvas"); + // canvas.width = 10; + // canvas.height = 10; + // const context = canvas.getContext("2d")!; + // context.fillRect(0, 0, 10, 10); + // const data = context.getImageData(5, 5, 1, 1).data; + // console.log(data); + // const [r, g, b] = data; + // const rgb = { r: r!, g: g!, b: b! }; + // _rgbCache[c] = rgb; + // return rgb; +}; + +const getTextColorForBackground = (c: string): string => { + // return "black"; + const { r, g, b } = getColorRgb(c); + const avg = (r + g + b) / 3 / 255; + console.log([r, g, b], avg); + return avg > 0.5 ? "black" : "white"; +}; + +const getColors = ( + types: DominionCardType[] +): { + primary: string; + secondary: string | null; + description: string | null; + descriptionText: string; + titleText: string; +} => { + const descriptionType = + types.find((t) => t.color?.onConflictDescriptionOnly) ?? null; + const byPriority = [...types] + .filter((type) => type.color && type !== descriptionType) + .sort((a, b) => b.color!.priority - a.color!.priority); + const priority1 = byPriority[0]!; + let primaryType: DominionCardType | null = priority1 ?? null; + let secondaryType = byPriority[1] ?? null; + if (priority1 === TYPE_ACTION) { + const overriders = byPriority.filter((t) => t.color!.overridesAction); + if (overriders.length) { + primaryType = overriders[0] ?? null; + } + if (primaryType === secondaryType) { + secondaryType = byPriority[2] ?? null; + } + } + primaryType = primaryType ?? descriptionType; + const primary = primaryType?.color?.value ?? "white"; + const secondary = secondaryType?.color?.value ?? null; + const description = descriptionType?.color?.value ?? null; + const descriptionText = getTextColorForBackground(description ?? primary); + const titleText = getTextColorForBackground(primary); + return { + primary, + secondary, + description, + descriptionText, + titleText, + }; +}; + +const drawStandardCard = async ( + context: CanvasRenderingContext2D, + card: DominionCard & { orientation: "card" } +): Promise => { + const w = context.canvas.width; + // const h = context.canvas.height; + let size; + context.save(); + // Draw the image + const image = await loadImage(card.image); + if (image) { + const cx = w / 2; + const cy = 704; + const windowHeight = 830; + const windowWidth = 1194; + const scale = Math.max( + windowHeight / image.height, + windowWidth / image.width + ); + context.drawImage( + image, + cx - (scale * image.width) / 2, + cy - (scale * image.height) / 2, + scale * image.width, + scale * image.height + ); + } + // Draw the card base + const colors = getColors(card.types); // "#ffbc55"; + + if (colors.secondary) { + context.drawImage( + colorImage(getImage("card-color-1"), colors.secondary), + 0, + 0 + ); + context.drawImage( + colorImage(getImage("card-color-2"), colors.primary), + 0, + 0 + ); + } else if (colors.description) { + context.drawImage( + colorImage(getImage("card-color-1"), colors.description), + 0, + 0 + ); + context.drawImage( + colorImage(getImage("card-color-2-night"), colors.primary), + 0, + 0 + ); + } else { + context.drawImage( + colorImage(getImage("card-color-1"), colors.primary), + 0, + 0 + ); + context.drawImage(getImage("card-description-focus"), 44, 1094); + } + context.drawImage(getImage("card-gray"), 0, 0); + context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0); + // Draw the name + context.fillStyle = colors.titleText; + context.font = "90pt DominionText"; + const previewMeasure = await measureDominionText( + context, + parse(card.preview ?? "") + ); + size = 78; + context.font = `${size}pt DominionTitle`; + while ( + (await measureDominionText(context, parse(card.title))).width > + 1050 - previewMeasure.width * 1.5 + ) { + size -= 1; + context.font = `${size}pt DominionTitle`; + } + await renderDominionText(context, parse(card.title), w / 2, 220); + // Draw the description + context.fillStyle = colors.descriptionText; + size = 60; + context.font = `${size}pt DominionText`; + while ( + ( + await measureDominionText( + context, + parse(card.description, { isDescription: true }), + 1000 + ) + ).height > 650 + ) { + size -= 1; + context.font = `${size}pt DominionText`; + } + await renderDominionText( + context, + parse(card.description, { isDescription: true }), + w / 2, + 1490, + 1000 + ); + // Draw the types + context.fillStyle = colors.titleText; + size = 65; + context.font = `${size}pt DominionTitle`; + while ( + ( + await measureDominionText( + context, + parse(card.types.map((t) => t.name).join(" - ")) + ) + ).width > 800 + ) { + size -= 1; + context.font = `${size}pt DominionTitle`; + } + await renderDominionText( + context, + parse(card.types.map((t) => t.name).join(" - ")), + w / 2, + 1930, + 800 + ); + // Draw the cost + context.fillStyle = colors.titleText; + context.font = "90pt DominionText"; + const costMeasure = await measureDominionText(context, parse(card.cost)); + await renderDominionText( + context, + parse(card.cost), + 130 + costMeasure.width / 2, + 1940 + ); + // Draw the preview + context.fillStyle = colors.titleText; + if (card.preview) { + context.font = "90pt DominionText"; + await renderDominionText(context, parse(card.preview), 200, 210); + await renderDominionText(context, parse(card.preview), w - 200, 210); + } + // Draw the expansion icon + // Draw the author credit + context.fillStyle = "white"; + context.font = "31pt DominionText"; + const authorMeasure = await measureDominionText( + context, + parse(card.author) + ); + await renderDominionText( + context, + parse(card.author), + w - 150 - authorMeasure.width / 2, + 2035 + ); + // Draw the artist credit + context.fillStyle = "white"; + const artistMeasure = await measureDominionText( + context, + parse(card.artist) + ); + await renderDominionText( + context, + parse(card.artist), + 155 + artistMeasure.width / 2, + 2035 + ); + // Restore the context + context.restore(); +}; + +const drawLandscapeCard = async ( + _context: CanvasRenderingContext2D, + _card: DominionCard & { orientation: "landscape" } +): Promise => { + // TODO: everything +}; diff --git a/src/fonthelper.ts b/src/fonthelper.ts new file mode 100644 index 0000000..ab7842d --- /dev/null +++ b/src/fonthelper.ts @@ -0,0 +1,41 @@ +import font from "npm:css-font"; + +export type FontInfo = { + style: "normal" | "italic" | "oblique"; + variant: "normal" | "small-caps"; + weight: + | "normal" + | "bold" + | "lighter" + | "bolder" + | "100" + | "200" + | "300" + | "400" + | "500" + | "600" + | "700" + | "800" + | "900"; + stretch: + | "normal" + | "condensed" + | "semi-condensed" + | "extra-condensed" + | "ultra-condensed" + | "expanded" + | "semi-expanded" + | "extra-expanded" + | "ultra-expanded"; + lineHeight: "normal" | number | string; + size: number | string; + family: string[]; +}; + +export const parseFont = (fontString: string): FontInfo => { + return { ...font.parse(fontString) }; +}; + +export const stringifyFont = (fontInfo: FontInfo): string => { + return font.stringify(fontInfo); +}; diff --git a/src/isocanvas.ts b/src/isocanvas.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/richtext.ts b/src/richtext.ts new file mode 100644 index 0000000..ca9edcc --- /dev/null +++ b/src/richtext.ts @@ -0,0 +1,25 @@ +// type RichnessNodeDefinition = { +// type: N["type"] +// measure(context: CanvasRenderingContext2D, node: N): Promise; +// render( +// context: CanvasRenderingContext2D, +// node: N, +// x: number, +// y: number +// ): Promise; +// }; + +// type Richness = {[K in N["type"]]: RichnessNodeDefinition} + +// const drawRichText = ( +// context: CanvasRenderingContext2D, +// richness: Richness, +// richText: N[], +// x: number, +// y: number, +// maxWidth: number, +// ) => { +// context.save(); +// const +// context.restore(); +// }; diff --git a/src/sampleData.ts b/src/sampleData.ts new file mode 100644 index 0000000..0e73904 --- /dev/null +++ b/src/sampleData.ts @@ -0,0 +1,78 @@ +import { + DominionCard, + TYPE_ACTION, + TYPE_DURATION, + TYPE_REACTION, + TYPE_TREASURE, + TYPE_VICTORY, +} from "./types.ts"; + +export const sampleCards: DominionCard[] = [ + { + orientation: "card", + title: "Title", + description: + "+*\n\nReveal the top card of your deck. If it's an Action card, +1 Action. If it has ^ in its cost, +1 Card.", + types: [TYPE_ACTION, TYPE_DURATION, TYPE_REACTION], + image: "https://wiki.dominionstrategy.com/images/7/76/AdventurerArt.jpg", + expansionIcon: "", + artist: "Dall-E", + author: "John Doe", + version: "", + cost: "@8", + preview: "", + }, + { + orientation: "card", + title: "Market", + description: "+1 Card\n+1 Action\n+1 Buy\n+$1", + types: [TYPE_ACTION], + image: "", + expansionIcon: "", + artist: "Leonardo DaVinci", + author: "Jane Smith", + version: "", + cost: "$4", + preview: "", + }, + { + orientation: "card", + title: "Flask", + description: + "+2 Cards\n\nAt the start of your Clean-up phase, you may put a card from your hand onto your deck.", + types: [TYPE_TREASURE], + image: "", + expansionIcon: "", + artist: "", + author: "", + version: "", + cost: "$6", + preview: "", + }, + { + orientation: "card", + title: "VP Card", + description: "Worth 1% per 3 cards you have that cost $4 or $5.", + types: [TYPE_VICTORY], + image: "", + expansionIcon: "", + artist: "", + author: "", + version: "", + cost: "$3", + preview: "", + }, + { + orientation: "card", + title: "Nobles", + description: "Choose one: +3 Cards, or +2 Actions.\n\n\n-\n\n\n2%", + types: [TYPE_ACTION, TYPE_VICTORY], + image: "", + expansionIcon: "", + artist: "", + author: "", + version: "", + cost: "$6", + preview: "", + }, +]; diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..f9334e0 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,14 @@ +import { serveDir, serveFile } from "jsr:@std/http/file-server"; + +Deno.serve((req: Request) => { + const pathname = new URL(req.url).pathname; + + if (pathname.startsWith("/static")) { + return serveDir(req, { + fsRoot: "static", + urlRoot: "static", + }); + } else { + return serveFile(req, "static/index.html"); + } +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9096c33 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,171 @@ +export type DominionText = string; + +export type DominionColor = { + value: string; + priority: number; // highest priority is "primary", second highest is "secondary". + overridesAction?: boolean; + onConflictDescriptionOnly?: boolean; +}; + +export type DominionBasicCardType = { + typeType: "basic"; + name: + | "Action" + | "Treasure" + | "Victory" + | "Curse" + | "Reaction" + | "Duration" + | "Reserve" + | "Night" + | "Attack" + | "Command"; + color: null | DominionColor; +}; +export type DominionBasicLandscapeType = { + typeType: "basic"; + name: "Event" | "Landmark" | "Project" | "Way" | "Trait"; + color: null | DominionColor; +}; + +export type DominionCardType = DominionBasicCardType | DominionCustomCardType; +export type DominionLandscapeType = + | DominionBasicLandscapeType + | DominionCustomLandscapeType; + +export type DominionCard = + | { + orientation: "card"; + title: string; + description: DominionText; + types: Array; + image: string; + artist: string; + author: string; + version: string; + cost: DominionText; + expansionIcon: string; + preview?: DominionText; + } + | { + orientation: "landscape"; + title: string; + description: DominionText; + types: Array; + image: string; + artist: string; + author: string; + version: string; + cost: DominionText; + }; + +export type DominionCustomSymbol = { + image: string; +}; + +export type DominionCustomCardType = { + typeType: "custom"; + name: string; + color: DominionColor; +}; +export type DominionCustomLandscapeType = { + typeType: "custom"; + name: string; + color: DominionColor; +}; + +export type DominionExpansion = { + cards: Array; + icon: string; + customSymbols: Array; + customCardTypes: Array; + customLandscapeTypes: Array; +}; + +export const TYPE_ACTION: DominionBasicCardType = { + typeType: "basic", + name: "Action", + color: { + value: "white", + priority: 6, + }, +}; + +export const TYPE_TREASURE: DominionBasicCardType = { + typeType: "basic", + name: "Treasure", + color: { + value: "#ffe076", + priority: 5, + }, +}; + +export const TYPE_VICTORY: DominionBasicCardType = { + typeType: "basic", + name: "Victory", + color: { + value: "#b3e5ad", + priority: 4, + }, +}; + +export const TYPE_CURSE: DominionBasicCardType = { + typeType: "basic", + name: "Curse", + color: { + value: "#d285ff", + priority: 4, + }, +}; + +export const TYPE_REACTION: DominionBasicCardType = { + typeType: "basic", + name: "Reaction", + color: { + value: "#81adff", + priority: 1, + overridesAction: true, + }, +}; + +export const TYPE_DURATION: DominionBasicCardType = { + typeType: "basic", + name: "Duration", + color: { + value: "#ffbc55", + priority: 3, + overridesAction: true, + }, +}; + +export const TYPE_RESERVE: DominionBasicCardType = { + typeType: "basic", + name: "Reserve", + color: { + value: "#e5c28b", + priority: 2, // unknown whether this should be above or below reaction/duration? + overridesAction: true, + }, +}; + +export const TYPE_NIGHT: DominionBasicCardType = { + typeType: "basic", + name: "Night", + color: { + value: "#485058", + priority: 6, + onConflictDescriptionOnly: true, + }, +}; + +export const TYPE_ATTACK: DominionBasicCardType = { + typeType: "basic", + name: "Attack", + color: null, +}; + +export const TYPE_COMMAND: DominionBasicCardType = { + typeType: "basic", + name: "Command", + color: null, +}; diff --git a/static/assets/BaseCardBrown.png b/static/assets/BaseCardBrown.png new file mode 100644 index 0000000..27fe253 Binary files /dev/null and b/static/assets/BaseCardBrown.png differ diff --git a/static/assets/BaseCardColorOne.png b/static/assets/BaseCardColorOne.png new file mode 100644 index 0000000..466e694 Binary files /dev/null and b/static/assets/BaseCardColorOne.png differ diff --git a/static/assets/BaseCardGray.png b/static/assets/BaseCardGray.png new file mode 100644 index 0000000..468bd4b Binary files /dev/null and b/static/assets/BaseCardGray.png differ diff --git a/static/assets/BaseCardIcon.png b/static/assets/BaseCardIcon.png new file mode 100644 index 0000000..0524d8c Binary files /dev/null and b/static/assets/BaseCardIcon.png differ diff --git a/static/assets/CardBrown.png b/static/assets/CardBrown.png new file mode 100644 index 0000000..944e861 Binary files /dev/null and b/static/assets/CardBrown.png differ diff --git a/static/assets/CardColorOne.png b/static/assets/CardColorOne.png new file mode 100644 index 0000000..0215d90 Binary files /dev/null and b/static/assets/CardColorOne.png differ diff --git a/static/assets/CardColorThree.png b/static/assets/CardColorThree.png new file mode 100644 index 0000000..4cdb8a7 Binary files /dev/null and b/static/assets/CardColorThree.png differ diff --git a/static/assets/CardColorTwo.png b/static/assets/CardColorTwo.png new file mode 100644 index 0000000..08d36a6 Binary files /dev/null and b/static/assets/CardColorTwo.png differ diff --git a/static/assets/CardColorTwoBig.png b/static/assets/CardColorTwoBig.png new file mode 100644 index 0000000..0d28ad3 Binary files /dev/null and b/static/assets/CardColorTwoBig.png differ diff --git a/static/assets/CardColorTwoNight.png b/static/assets/CardColorTwoNight.png new file mode 100644 index 0000000..e5f5a6e Binary files /dev/null and b/static/assets/CardColorTwoNight.png differ diff --git a/static/assets/CardColorTwoSmall.png b/static/assets/CardColorTwoSmall.png new file mode 100644 index 0000000..fd49557 Binary files /dev/null and b/static/assets/CardColorTwoSmall.png differ diff --git a/static/assets/CardGray.png b/static/assets/CardGray.png new file mode 100644 index 0000000..9ed4a07 Binary files /dev/null and b/static/assets/CardGray.png differ diff --git a/static/assets/CardPortraitIcon.png b/static/assets/CardPortraitIcon.png new file mode 100644 index 0000000..30d25b2 Binary files /dev/null and b/static/assets/CardPortraitIcon.png differ diff --git a/static/assets/Coin.png b/static/assets/Coin.png new file mode 100644 index 0000000..13d9a7f Binary files /dev/null and b/static/assets/Coin.png differ diff --git a/static/assets/Debt.png b/static/assets/Debt.png new file mode 100644 index 0000000..63c6ed2 Binary files /dev/null and b/static/assets/Debt.png differ diff --git a/static/assets/DescriptionFocus.png b/static/assets/DescriptionFocus.png new file mode 100644 index 0000000..697cb93 Binary files /dev/null and b/static/assets/DescriptionFocus.png differ diff --git a/static/assets/DoubleColorOne.png b/static/assets/DoubleColorOne.png new file mode 100644 index 0000000..36d01d6 Binary files /dev/null and b/static/assets/DoubleColorOne.png differ diff --git a/static/assets/DoubleUncoloredDetails.png b/static/assets/DoubleUncoloredDetails.png new file mode 100644 index 0000000..2449445 Binary files /dev/null and b/static/assets/DoubleUncoloredDetails.png differ diff --git a/static/assets/EventBrown.png b/static/assets/EventBrown.png new file mode 100644 index 0000000..69a0247 Binary files /dev/null and b/static/assets/EventBrown.png differ diff --git a/static/assets/EventBrown2.png b/static/assets/EventBrown2.png new file mode 100644 index 0000000..6e845ee Binary files /dev/null and b/static/assets/EventBrown2.png differ diff --git a/static/assets/EventColorOne.png b/static/assets/EventColorOne.png new file mode 100644 index 0000000..1f38e93 Binary files /dev/null and b/static/assets/EventColorOne.png differ diff --git a/static/assets/EventColorTwo.png b/static/assets/EventColorTwo.png new file mode 100644 index 0000000..f4e3af9 Binary files /dev/null and b/static/assets/EventColorTwo.png differ diff --git a/static/assets/EventHeirloom.png b/static/assets/EventHeirloom.png new file mode 100644 index 0000000..0389fd6 Binary files /dev/null and b/static/assets/EventHeirloom.png differ diff --git a/static/assets/Heirloom.png b/static/assets/Heirloom.png new file mode 100644 index 0000000..88d3aae Binary files /dev/null and b/static/assets/Heirloom.png differ diff --git a/static/assets/MatBannerBottom.png b/static/assets/MatBannerBottom.png new file mode 100644 index 0000000..9f8eb87 Binary files /dev/null and b/static/assets/MatBannerBottom.png differ diff --git a/static/assets/MatBannerTop.png b/static/assets/MatBannerTop.png new file mode 100644 index 0000000..50a5d21 Binary files /dev/null and b/static/assets/MatBannerTop.png differ diff --git a/static/assets/MatIcon.png b/static/assets/MatIcon.png new file mode 100644 index 0000000..b12dc14 Binary files /dev/null and b/static/assets/MatIcon.png differ diff --git a/static/assets/PileMarkerColorOne.png b/static/assets/PileMarkerColorOne.png new file mode 100644 index 0000000..e874d57 Binary files /dev/null and b/static/assets/PileMarkerColorOne.png differ diff --git a/static/assets/PileMarkerGrey.png b/static/assets/PileMarkerGrey.png new file mode 100644 index 0000000..7fbd8ed Binary files /dev/null and b/static/assets/PileMarkerGrey.png differ diff --git a/static/assets/PileMarkerIcon.png b/static/assets/PileMarkerIcon.png new file mode 100644 index 0000000..f313aaa Binary files /dev/null and b/static/assets/PileMarkerIcon.png differ diff --git a/static/assets/Potion.png b/static/assets/Potion.png new file mode 100644 index 0000000..90cf604 Binary files /dev/null and b/static/assets/Potion.png differ diff --git a/static/assets/Sun.png b/static/assets/Sun.png new file mode 100644 index 0000000..4648da1 Binary files /dev/null and b/static/assets/Sun.png differ diff --git a/static/assets/TraitBrown.png b/static/assets/TraitBrown.png new file mode 100644 index 0000000..b21ea5b Binary files /dev/null and b/static/assets/TraitBrown.png differ diff --git a/static/assets/TraitBrownSide.png b/static/assets/TraitBrownSide.png new file mode 100644 index 0000000..9cecd4b Binary files /dev/null and b/static/assets/TraitBrownSide.png differ diff --git a/static/assets/TraitColorOne.png b/static/assets/TraitColorOne.png new file mode 100644 index 0000000..201b036 Binary files /dev/null and b/static/assets/TraitColorOne.png differ diff --git a/static/assets/TraitColorOneSide.png b/static/assets/TraitColorOneSide.png new file mode 100644 index 0000000..55db55f Binary files /dev/null and b/static/assets/TraitColorOneSide.png differ diff --git a/static/assets/Traveller.png b/static/assets/Traveller.png new file mode 100644 index 0000000..c88255f Binary files /dev/null and b/static/assets/Traveller.png differ diff --git a/static/assets/VP-Token.png b/static/assets/VP-Token.png new file mode 100644 index 0000000..5f267fc Binary files /dev/null and b/static/assets/VP-Token.png differ diff --git a/static/assets/VP.png b/static/assets/VP.png new file mode 100644 index 0000000..108ded4 Binary files /dev/null and b/static/assets/VP.png differ diff --git a/static/fonts.css b/static/fonts.css new file mode 100644 index 0000000..433a4e6 --- /dev/null +++ b/static/fonts.css @@ -0,0 +1,27 @@ +/* @font-face { + font-family: 'DominionTitle'; + font-display: block; + src: local("Trajan Pro Bold"), local("TrajanPro-Bold"), local('Trajan Pro'), + url('https://fonts.cdnfonts.com/s/14928/TrajanPro-Bold.woff') format('woff'), + url('https://shemitz.net/static/dominion3/Trajan%20Pro%20Bold.ttf') format('truetype'), + url('https://dominion.games/fonts/TrajanPro-Bold.otf') format('opentype'), + local("Trajan"), + local("Optimus Princeps"), + url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2'); +} */ + +@font-face { + font-family: 'DominionText'; + font-display: block; + src: local("Times New Roman"), serif; +} + +/* @font-face { + font-family: 'DominionSpecial'; + font-display: block; + src: local("Minion Std Black"), local("MinionStd-Black"), local("Minion Std"), local('Minion Pro'), + url('https://fonts.cdnfonts.com/s/13260/MinionPro-Regular.woff') format('woff'), + url('https://shemitz.net/static/dominion3/MinionStd-Black.otf') format('opentype'), + local("Optimus Princeps"), + url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2'); +} */ \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..c22816a --- /dev/null +++ b/static/index.html @@ -0,0 +1,13 @@ + + + + + + Dominionator + + + +
+ + + diff --git a/tools/build.ts b/tools/build.ts new file mode 100644 index 0000000..3f29d7c --- /dev/null +++ b/tools/build.ts @@ -0,0 +1,28 @@ +import * as esbuild from "npm:esbuild"; +import { denoPlugins } from "jsr:@luca/esbuild-deno-loader"; +import browserslist from "npm:browserslist"; +import { projectRootDir } from "../root.ts"; + +const browsers = browserslist([ + "last 4 Chrome versions", + "last 4 Edge versions", + "last 4 Opera versions", + "last 4 Firefox versions", + "last 4 Safari versions", +]).map((browser: string) => browser.replace(" ", "")); + +// esbuild target is fine-grained: https://esbuild.github.io/api/#target +const target = [...browsers, "ios18", "ios17", "ios16", "ios14"]; +await esbuild.build({ + plugins: [...denoPlugins()], + absWorkingDir: projectRootDir, + entryPoints: ["src/client/index.tsx"], + outfile: "static/dist/bundle.js", + bundle: true, + format: "esm", + target, + jsx: "automatic", + jsxImportSource: "react", +}); + +esbuild.stop(); diff --git a/tools/dev.ts b/tools/dev.ts new file mode 100644 index 0000000..c96c8db --- /dev/null +++ b/tools/dev.ts @@ -0,0 +1,37 @@ +runConcurrentTasks().catch((error) => { + console.error("Error running tasks:", error); + Deno.exit(1); +}); + +async function runConcurrentTasks() { + const tasks = [ + runCommand("deno task build:watch"), + runCommand("deno task serve:watch"), + ]; + + const results = await Promise.all(tasks); + if (results.includes(false)) { + console.error("One or more tasks failed."); + Deno.exit(1); + } else { + console.log("All tasks completed successfully."); + } +} + +async function runCommand(fullCommand: string) { + const [command, ...args] = fullCommand.split(" "); + + const cmd = new Deno.Command(command!, { + args, + stdout: "piped", + }); + const process = cmd.spawn(); + + const status = await process.status; + if (status.code !== 0) { + console.error(`Command failed: ${command}`); + return false; + } + + return true; +}