{"id":367,"date":"2024-05-03T04:06:22","date_gmt":"2024-05-03T04:06:22","guid":{"rendered":"https:\/\/trewest.dev\/blog\/?p=367"},"modified":"2024-05-03T04:06:22","modified_gmt":"2024-05-03T04:06:22","slug":"blood-on-the-clocktower-icon-generation","status":"publish","type":"post","link":"https:\/\/trewest.dev\/blog\/index.php\/2024\/05\/03\/blood-on-the-clocktower-icon-generation\/","title":{"rendered":"Blood on the Clocktower Icon Generation"},"content":{"rendered":"\n<p>This is a quick passion project of mine, two simple scripts that generate icons for a custom role list for a game called <em>Blood on the Clocktower.<\/em>  Blood on the Clocktower is a fun board game with an online version available for free.  My homebrew script <em>Seeing Double<\/em> required me to make icons of two normal characters and combine them.  I decided to do this by taking two opposite halves of the two icons at a 45 degree angle and putting them together.  Originally, I did this manually in Photoshop, but decided that writing a script for it might be faster.  And so, I decided to look up popular image manipulation libraries, the majority of which were written for JavaScript.  <br>Eventually, I came across <a href=\"https:\/\/github.com\/jimp-dev\/jimp\" data-type=\"link\" data-id=\"https:\/\/github.com\/jimp-dev\/jimp\">JIMP, Javascript Image Manipulation Program<\/a>, and decided that it&#8217;d be the right thing for the job.  In around an hour of trial and error, I had made a working script&#8230; but forgotten that one hundred fourty four squared is a very large number, and I had fed my script that many images to make every possible variation.  Not because I needed them but I was already downloading around 60 images anyways, might as well download all of them.  And so from 20 megabytes of input, I received 3 gigabytes in return.  <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"742\" height=\"504\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image.png\" alt=\"\" class=\"wp-image-368\" srcset=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image.png 742w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-300x204.png 300w\" sizes=\"auto, (max-width: 742px) 100vw, 742px\" \/><figcaption class=\"wp-element-caption\">Maybe this wasn&#8217;t such a great idea. (Also there&#8217;s a bit of variation from having to fix some issues)<\/figcaption><\/figure>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"100\" height=\"100\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/combined.png\" alt=\"\" class=\"wp-image-369\"\/><figcaption class=\"wp-element-caption\">I used two single color images to test the tool.<\/figcaption><\/figure>\n<\/div>\n\n\n<p>And finally, I had a great big variety of possible seeing double tokens.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"547\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-1-1024x547.png\" alt=\"\" class=\"wp-image-371\" srcset=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-1-1024x547.png 1024w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-1-300x160.png 300w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-1-768x410.png 768w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-1-1536x821.png 1536w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/image-1.png 1645w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>But this wasn&#8217;t enough.  After this I realized I needed varieties of each of these being good and evil.  If I did all possible options I&#8217;d be dangerously close to six gigabytes of icons, so I decided to just do what I needed this time which made this iteration much faster.   I thought hard about how to make the correct look for each token.  I could detect white, grey out the rest and then multiply it with a color, but that seemed to be a lot of work.  Then I thought about making colored noise fill it, which I tried but didn&#8217;t look right.  The example below shows the original good versus the new evil.<\/p>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"591\" height=\"591\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother-1.png\" alt=\"\" class=\"wp-image-374\" srcset=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother-1.png 591w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother-1-300x300.png 300w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother-1-150x150.png 150w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"591\" height=\"591\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_evil-1.png\" alt=\"\" class=\"wp-image-375\" srcset=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_evil-1.png 591w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_evil-1-300x300.png 300w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_evil-1-150x150.png 150w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/figure>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:100%\">\n<p>Sometimes though, the simplest solution is the best.  And thankfully someone had already made a good noise fill for a majority of things with their own token maker.  So I just borrowed a square of that for evil and good and used that as the fill image instead, below are the results of good and evil of the same image.<\/p>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"591\" height=\"591\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_good-1.png\" alt=\"\" class=\"wp-image-378\" srcset=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_good-1.png 591w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_good-1-300x300.png 300w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_good-1-150x150.png 150w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"591\" height=\"591\" src=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_evil.png\" alt=\"\" class=\"wp-image-379\" srcset=\"https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_evil.png 591w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_evil-300x300.png 300w, https:\/\/trewest.dev\/blog\/wp-content\/uploads\/2024\/05\/Cannimother_good_evil-150x150.png 150w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/figure>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<p>While obviously not an exact match, the soft (what is probably Perlin) noise has a pretty good color match and looks similar enough at a distance to look accurate, which is really what matters.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Scripts<\/h2>\n\n\n\n<p>Token maker<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import jimp from \"jimp\"\nimport path from \"node:path\"\nimport fs from \"node:fs\"\n\nconst offset = 10;\nconst inputFolder = 'icons\/';\nconst outputFolder = 'out\/';\n\n\nasync function main( ) \n{\n    \/\/Making a list of every image in the input folder\n    let list = &#91;];\n    fs.readdirSync(inputFolder).forEach(file => {list&#91;list.length] = file;})\n\n    \/\/ For every image, make a new image with every other image.\n    for (let i =0;i&lt;list.length;i++) \n    {\n        \/\/await combine(\"organgrinder.png\",list&#91;i]); This is a lazy way of fixing individual variations\n        \/\/await combine(list&#91;i],\"organgrinder.png\");  When fixing individual ones, I'd just comment out the second for loop.\n\n        for (let x = 0;x&lt;list.length;x++) \n        {\n            await combine(list&#91;i], list&#91;x]);\n        }\n    }\n    \n}\n\nasync function combine(top, bottom) {\n    \/\/ We start by reading the first \/ the \"top\" image\n    const image = await jimp.read(inputFolder + top);\n\n    const topimg = image.scanQuiet(\n        0,\n        0,\n        image.bitmap.width,\n        image.bitmap.height,\n        function (x, y, idx) {\n                if (x +offset >=  (image.bitmap.width - y)) \/\/ When X + the offset is greater than the width minus y we will take the pixels.  \n                {\n                    image.bitmap.data&#91;idx] =0;\n                    image.bitmap.data&#91;idx+1] =0;\n                    image.bitmap.data&#91;idx+2] =0;\n                    image.bitmap.data&#91;idx+3] =0;\n                }\n              }\n      );\n      \/\/ We start by reading the first \/ the \"top\" image\n    const image2 = await jimp.read(inputFolder + bottom);\n    const BOTimg = image2.scanQuiet(\n        0,\n        0,\n        image2.bitmap.width,\n        image2.bitmap.height,\n        function (x, y, idx) {\n                if (x &lt;=  (image2.bitmap.width - y + offset))  \/\/ When X is less than width minus y plus the offset we take the second half of the image data.\n                {\n                    image2.bitmap.data&#91;idx] =0;\n                    image2.bitmap.data&#91;idx+1] =0;\n                    image2.bitmap.data&#91;idx+2] =0;\n                    image2.bitmap.data&#91;idx+3] =0;\n                }\n              }\n      );\n      \/\/After saving both halves of the image we'll combine them like this quickly.\n    const Combined = topimg.scanQuiet(0,0,topimg.bitmap.width, topimg.bitmap.height,\n        function (x,y,idx) \n        {\n            topimg.bitmap.data&#91;idx] += BOTimg.bitmap.data&#91;idx];\n            topimg.bitmap.data&#91;idx+1] += BOTimg.bitmap.data&#91;idx+1];\n            topimg.bitmap.data&#91;idx+2] += BOTimg.bitmap.data&#91;idx+2];\n            topimg.bitmap.data&#91;idx+3] += BOTimg.bitmap.data&#91;idx+3];\n        }\n        );\n    await Combined.writeAsync('out\/'+ top + '_and_' + bottom);\n\n\n}\nawait main();\nconsole.log(\"test\");<\/code><\/pre>\n\n\n\n<p>Recolor<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import jimp from \"jimp\"\nimport path from \"node:path\"\nimport fs from \"node:fs\"\n\nconst offset = 10; \/\/ unused\nconst folder = 'Copiloted\/'; \/\/ Input folder\nconst folder_out = 'copiloted_out\/'; \/\/ Output folder\nconst noise = 'noise.png'; \/\/ unused\nconst noise_blue = 'noise_blue3.png' \/\/ Image for good player background\nconst noise_red = 'noise_red3.png' \/\/ Image for evil player background\nconst white_min = 160; \/\/ The minimum for colors to be considered in the white\/grey zone for \nlet total = 0; \/\/ total iterations\/generations for console writing.  \n\n\nasync function main( ) \n{\n    let list = &#91;];\n    \n    fs.readdirSync(folder).forEach(file => {list&#91;list.length] = path.basename(file, path.extname(file));})\n\n    \/\/await recolor('slayight_fixed');  Recoloring single \n    \n    for (let i =0;i&lt;list.length;i++) \n    {\n        await recolor(list&#91;i]);\n        total+=2;\n    }\n    \n}\n\nasync function recolor(top) {\n    \n    const read_red = await jimp.read(noise_red);\n    const read_blue = await jimp.read(noise_blue); \n    const image = await jimp.read(folder + top + '.png');\n    image.resize(read_red.bitmap.height,read_red.bitmap.width,jimp.RESIZE_BICUBIC);\n\n    const blue_image = image.scanQuiet(\n        0,\n        0,\n        image.bitmap.width,\n        image.bitmap.height,\n        function (x, y, idx) {\n            if (image.bitmap.data&#91;idx] > white_min &amp;&amp; image.bitmap.data&#91;idx+1] >white_min &amp;&amp; image.bitmap.data&#91;idx+2] > white_min)\n            {\n                \n                image.bitmap.data&#91;idx] = image.bitmap.data&#91;idx];\/\/ red channel\n                image.bitmap.data&#91;idx+1] = image.bitmap.data&#91;idx+1]; \/\/ blue channel\n                image.bitmap.data&#91;idx+2] = image.bitmap.data&#91;idx+2]; \/\/ green channel\n            }\n            else if (image.bitmap.data&#91;idx+3] > 50)\n            {\n                \n                image.bitmap.data&#91;idx] = read_blue.bitmap.data&#91;idx];\n                image.bitmap.data&#91;idx+1] = read_blue.bitmap.data&#91;idx+1];\n                image.bitmap.data&#91;idx+2] = read_blue.bitmap.data&#91;idx+2];\n                image.bitmap.data&#91;idx+3] = image.bitmap.data&#91;idx+3]; \/\/ alpha\n            }\n            }\n      );\n      console.log(`Writing ${folder_out + top + '_good.png'}`);\n      await blue_image.writeAsync(folder_out + top + '_good.png');\n\n      const red_image = image.scanQuiet(\n        0,\n        0,\n        image.bitmap.width,\n        image.bitmap.height,\n        function (x, y, idx) {\n            if (image.bitmap.data&#91;idx] > white_min &amp;&amp; image.bitmap.data&#91;idx+1] >white_min &amp;&amp; image.bitmap.data&#91;idx+2] > white_min)\n            {\n                \n                image.bitmap.data&#91;idx] = image.bitmap.data&#91;idx];\n                image.bitmap.data&#91;idx+1] = image.bitmap.data&#91;idx+1];\n                image.bitmap.data&#91;idx+2] = image.bitmap.data&#91;idx+2];\n            }\n            else if (image.bitmap.data&#91;idx+3] > 50)\n            {\n                \n                image.bitmap.data&#91;idx] = read_red.bitmap.data&#91;idx];\n                image.bitmap.data&#91;idx+1] = read_red.bitmap.data&#91;idx+1];\n                image.bitmap.data&#91;idx+2] = read_red.bitmap.data&#91;idx+2];\n                image.bitmap.data&#91;idx+3] = image.bitmap.data&#91;idx+3];\n            }\n            }\n      );\n      console.log(`Writing ${folder_out + top + '_evil.png'}`);\n      await red_image.writeAsync(folder_out + top + '_evil.png');\n    \n\n\n}\nawait main();\nconsole.log(\"Test complete.\");\nconsole.log(`Images Generated: ${total}.`);<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>This is a quick passion project of mine, two simple scripts that generate icons for a custom role list for a game called Blood on the Clocktower. Blood on the &#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,42,18,8],"tags":[12,17,74,75],"post_series":[],"class_list":["post-367","post","type-post","status-publish","format-standard","hentry","category-coding","category-js","category-other","category-project","tag-code","tag-graphics","tag-javascript","tag-node-js"],"_links":{"self":[{"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/posts\/367","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=367"}],"version-history":[{"count":2,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/posts\/367\/revisions"}],"predecessor-version":[{"id":381,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/posts\/367\/revisions\/381"}],"wp:attachment":[{"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=367"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=367"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=367"},{"taxonomy":"post_series","embeddable":true,"href":"https:\/\/trewest.dev\/blog\/index.php\/wp-json\/wp\/v2\/post_series?post=367"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}