Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# mod-generator
Generate mod images using warframe-items

![Afterburn](https://raw.githubusercontent.com/WFCD/mod-generator/main/assets/readme/Afterburn.png)
![Steel Charge](https://raw.githubusercontent.com/WFCD/mod-generator/main/assets/readme/Steel_Charge.png)
![Vitality](https://raw.githubusercontent.com/WFCD/mod-generator/main/assets/readme/Vitality.png)
![Archgun Riven Mod]((https://raw.githubusercontent.com/WFCD/mod-generator/main/assets/readme/Archgun_Riven_Mod.png))
![Primed Flow]((https://raw.githubusercontent.com/WFCD/mod-generator/main/assets/readme/Archgun_Riven_Mod.png))

[![Supported by the Warframe Community Developers](https://img.shields.io/badge/Warframe_Comm_Devs-supported-blue.svg?color=2E96EF&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTgiIGhlaWdodD0iMTczIiB2aWV3Qm94PSIwIDAgMjk4IDE3MyI%2BPHBhdGggZD0iTTE4NSA2N2MxNSA4IDI4IDE2IDMxIDE5czIzIDE4LTcgNjBjMCAwIDM1LTMxIDI2LTc5LTE0LTctNjItMzYtNzAtNDUtNC01LTEwLTEyLTE1LTIyLTUgMTAtOSAxNC0xNSAyMi0xMyAxMy01OCAzOC03MiA0NS05IDQ4IDI2IDc5IDI2IDc5LTMwLTQyLTEwLTU3LTctNjBsMzEtMTkgMzYtMjIgMzYgMjJ6TTU1IDE3M2wtMTctM2MtOC0xOS0yMC00NC0yNC01MC01LTctNy0xMS0xNC0xNWwxOC0yYzE2LTMgMjItNyAzMi0xMyAxIDYgMCA5IDIgMTQtNiA0LTIxIDEwLTI0IDE2IDMgMTQgNSAyNyAyNyA1M3ptMTYtMTFsLTktMi0xNC0yOWEzMCAzMCAwIDAgMC04LThoN2wxMy00IDQgN2MtMyAyLTcgMy04IDZhODYgODYgMCAwIDAgMTUgMzB6bTE3MiAxMWwxNy0zYzgtMTkgMjAtNDQgMjQtNTAgNS03IDctMTEgMTQtMTVsLTE4LTJjLTE2LTMtMjItNy0zMi0xMy0xIDYgMCA5LTIgMTQgNiA0IDIxIDEwIDI0IDE2LTMgMTQtNSAyNy0yNyA1M3ptLTE2LTExbDktMiAxNC0yOWEzMCAzMCAwIDAgMSA4LThoLTdsLTEzLTQtNCA3YzMgMiA3IDMgOCA2YTg2IDg2IDAgMCAxLTE1IDMwem0tNzktNDBsLTYtNmMtMSAzLTMgNi02IDdsNSA1YTUgNSAwIDAgMSAyIDB6bS0xMy0yYTQgNCAwIDAgMSAxLTJsMi0yYTQgNCAwIDAgMSAyLTFsNC0xNy0xNy0xMC04IDcgMTMgOC0yIDctNyAyLTgtMTItOCA4IDEwIDE3em0xMiAxMWE1IDUgMCAwIDAtNC0yIDQgNCAwIDAgMC0zIDFsLTMwIDI3YTUgNSAwIDAgMCAwIDdsNCA0YTYgNiAwIDAgMCA0IDIgNSA1IDAgMCAwIDMtMWwyNy0zMWMyLTIgMS01LTEtN3ptMzkgMjZsLTMwLTI4LTYgNmE1IDUgMCAwIDEgMCAzbDI2IDI5YTEgMSAwIDAgMCAxIDBsNS0yIDItMmMxLTIgMy01IDItNnptNS00NWEyIDIgMCAwIDAtNCAwbC0xIDEtMi00YzEtMy01LTktNS05LTEzLTE0LTIzLTE0LTI3LTEzLTIgMS0yIDEgMCAyIDE0IDIgMTUgMTAgMTMgMTNhNCA0IDAgMCAwLTEgMyAzIDMgMCAwIDAgMSAxbC0yMSAyMmE3IDcgMCAwIDEgNCAyIDggOCAwIDAgMSAyIDNsMjAtMjFhNyA3IDAgMCAwIDEgMSA0IDQgMCAwIDAgNCAwYzEtMSA2IDMgNyA0aC0xYTMgMyAwIDAgMCAwIDQgMiAyIDAgMCAwIDQgMGw2LTZhMyAzIDAgMCAwIDAtM3oiIGZpbGw9IiMyZTk2ZWYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D)](https://github.com/WFCD/banner/blob/master/PROJECTS.md)
[![Coverage Status](https://coveralls.io/repos/github/WFCD/warframe-worldstate-parser/badge.svg?branch=master)](https://coveralls.io/github/WFCD/profile-parser?branch=master)
[![Discord](https://img.shields.io/discord/256087517353213954.svg?logo=discord)](https://discord.gg/jGZxH9f)
[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)

# mod-generator
Generate mod images using warframe-items

![Afterburn](/assets/readme/Afterburn.png)
![Steel Charge](/assets/readme/Steel_Charge.png)
![Vitality](/assets/readme/Vitality.png)
![Archgun Riven Mod](/assets/readme/Archgun_Riven_Mod.png)
![Primed Flow](/assets/readme/Primed_Flow.png)

## Documentation

You can find the documentation [here](https://wfcd.github.io/mod-generator/)
Expand Down
Binary file modified assets/readme/Afterburn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme/Archgun_Riven_Mod.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme/Primed_Flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme/Steel_Charge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme/Vitality.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": {
"ignoreUnknown": true,
"includes": ["test/**/*.ts", "src/**/*.ts"]
"includes": ["tests/**/*.spec.ts", "src/**/*.ts"]
},
"formatter": {
"enabled": true,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
]
},
"mocha": {
"timeout": 20000,
"timeout": 22000,
"exit": true,
"node-option": [
"import=tsx"
Expand Down
91 changes: 50 additions & 41 deletions src/drawers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createCanvas, type Image, loadImage } from '@napi-rs/canvas';
import { type Canvas, createCanvas, type Image, loadImage } from '@napi-rs/canvas';
import type { Mod } from 'warframe-items';

import {
Expand All @@ -9,13 +9,14 @@ import {
modDescription,
modRarityMap,
textColor,
textHeight,
wrapText,
} from './utils.js';

export const verticalPad = 70;
export const horizantalPad = 7;
export const horizantalPad = 8;

const drawPolarity = async (tier: string, polarity: string): Promise<Image> => {
const drawPolarity = async (tier: string, polarity: string): Promise<Canvas> => {
const image = await fetchPolarity(polarity);

const size = 32;
Expand All @@ -29,7 +30,7 @@ const drawPolarity = async (tier: string, polarity: string): Promise<Image> => {
context.fillStyle = textColor(tier);
context.fillRect(0, 0, size, size);

return loadImage(await canvas.encode('png'));
return canvas;
};

/**
Expand All @@ -53,7 +54,7 @@ interface BackerImageProps {
* @param {BackerImageProps} props Props used when creating backer image
* @returns {Promise<Image>}
*/
export const backerImage = async (props: BackerImageProps): Promise<Image> => {
export const backerImage = async (props: BackerImageProps): Promise<Canvas> => {
const { backer, tier, base, polarity, rank } = props;
const canvas = createCanvas(backer.width, backer.height);
const context = canvas.getContext('2d');
Expand All @@ -66,7 +67,7 @@ export const backerImage = async (props: BackerImageProps): Promise<Image> => {
const drainHeight = canvas.height * 0.7;
if (tier === modRarityMap.riven) {
context.fillText('???', canvas.width * 0.4, drainHeight);
return loadImage(await canvas.encode('png'));
return canvas;
}

if (polarity === 'universal') {
Expand All @@ -84,7 +85,7 @@ export const backerImage = async (props: BackerImageProps): Promise<Image> => {
context.fillText(`${drain}`, canvas.width * 0.35, drainHeight);
}

return loadImage(await canvas.encode('png'));
return canvas;
};

/**
Expand All @@ -101,7 +102,7 @@ interface LowerTabProps {
* @param {LowerTabProps} props Props used in creating the lower tab
* @returns {Promise<Image>}
*/
export const lowerTabImage = async (props: LowerTabProps): Promise<Image> => {
export const lowerTabImage = async (props: LowerTabProps): Promise<Canvas> => {
const { lowerTab, tier, compatName } = props;
const canvas = createCanvas(lowerTab.width, lowerTab.height);
const context = canvas.getContext('2d');
Expand All @@ -117,7 +118,7 @@ export const lowerTabImage = async (props: LowerTabProps): Promise<Image> => {
context.fillText(compatName, canvas.width * 0.5, canvas.height * 0.5);
}

return loadImage(await canvas.encode('png'));
return canvas;
};

/**
Expand All @@ -139,20 +140,54 @@ interface BackgroundProps {
* @param {BackerImageProps} props Props used in create the background image
* @returns {Promise<Image>}
*/
export const backgroundImage = async (props: BackgroundProps): Promise<Image> => {
export const backgroundImage = async (props: BackgroundProps): Promise<Canvas> => {
const { background, sideLights, backer, lowerTab, bottom, mod, rank, image } = props;
const tier = getTier(mod);
const canvas = createCanvas(background.width, background.height);
const context = canvas.getContext('2d');

context.drawImage(background, 0, 0);

const maxWidth = background.width * 0.8;
const description = modDescription(mod.description, mod.levelStats, rank ?? 0);
const lines = description?.split('\n');

context.font = '12px "Roboto"';
const modTextHeight = textHeight(context, maxWidth, mod.name, lines);

// Track Y after image is drawn to know where to start drawing the text
let position = canvas.height * 0.17;
if (mod.imageName || image) {
const thumb = await loadImage(image ?? `https://cdn.warframestat.us/img/${mod.imageName}`);
const thumbWidth = canvas.width - horizantalPad * 2;
const thumbHeight = 170;
const thumbHeight = thumb.height - modTextHeight;

context.drawImage(thumb, horizantalPad, position, thumbWidth, thumbHeight);

position += thumbHeight;
}

context.fillStyle = textColor(tier);
context.textAlign = 'center';
context.textBaseline = 'middle';
context.font = '400 16px "Roboto"';
context.fillText(mod.name, canvas.width * 0.5, position + horizantalPad * 2);

context.drawImage(thumb, horizantalPad, canvas.height * 0.17, thumbWidth, thumbHeight);
position += horizantalPad * 2;
if (description && description.length > 0) {
const x = canvas.width * 0.5;

context.font = '12px "Roboto"';
const lineSpacing = 15;
let start = position + horizantalPad * 2;

lines?.forEach((line) => {
const texts = wrapText(context, line, maxWidth);
texts.forEach((text) => {
context.fillText(text, x, start, maxWidth);
start += lineSpacing;
});
});
}

const sideLightsY = background.height * 0.21;
Expand Down Expand Up @@ -181,33 +216,7 @@ export const backgroundImage = async (props: BackgroundProps): Promise<Image> =>
background.height - bottom.height - padding
);

context.fillStyle = textColor(tier);

context.textAlign = 'center';
context.textBaseline = 'middle';
context.font = '400 16px "Roboto"';

context.fillText(mod.name, canvas.width * 0.5, canvas.height * 0.52);

const description = modDescription(mod.description, mod.levelStats, rank ?? 0);
if (description && description.length > 0) {
const x = canvas.width * 0.5;
const lines = description.split('\n');

context.font = '12px "Roboto"';
let start = canvas.height * 0.56;

lines.forEach((line) => {
const maxWidth = background.width * 0.8;
const texts = wrapText(context, line, maxWidth);
texts.forEach((text) => {
context.fillText(text, x, start, maxWidth);
start += 15;
});
});
}

return loadImage(await canvas.encode('png'));
return canvas;
};

/**
Expand All @@ -228,7 +237,7 @@ interface BottomImageProps {
* @param {BottomImageProps} props Props used in the creation of the lower frame
* @returns {Promise<Image>}
*/
export const bottomImage = async (props: BottomImageProps): Promise<Image> => {
export const bottomImage = async (props: BottomImageProps): Promise<Canvas> => {
const { bottom, cornerLights, tier, max, rank } = props;

const rankSlotEmpy = await fetchModPiece('RankSlotEmpty.png');
Expand Down Expand Up @@ -276,5 +285,5 @@ export const bottomImage = async (props: BottomImageProps): Promise<Image> => {
rankSlotStart += 11;
}

return loadImage(await canvas.encode('png'));
return canvas;
};
6 changes: 3 additions & 3 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createCanvas } from '@napi-rs/canvas';
import type { Mod } from 'warframe-items';

import { backgroundImage, bottomImage, horizantalPad, verticalPad } from './drawers.js';
import { backgroundImage, bottomImage, horizantalPad } from './drawers.js';
import {
type CanvasOutput,
exportCanvas,
Expand Down Expand Up @@ -76,7 +76,7 @@ const generate = async (
}

if (bottom.width > background.width) {
const newXPadding = horizantalPad * 6;
const newXPadding = horizantalPad * 5;
const widthDiff = bottom.width - background.width - newXPadding;
context.drawImage(
await bottomImage({
Expand All @@ -103,7 +103,7 @@ const generate = async (
);
}

const outterCanvas = createCanvas(isRiven ? 292 : 256, 512 - verticalPad);
const outterCanvas = createCanvas(isRiven ? 292 : 256, 380);
const outterContext = outterCanvas.getContext('2d');

outterContext.drawImage(canvas, (outterCanvas.width - canvas.width) / 2, (outterCanvas.height - canvas.height) / 2);
Expand Down
21 changes: 20 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,28 @@ export const exportCanvas = async (canvas: Canvas, output: CanvasOutput = { form
case 'jpeg':
return await canvas.encode('jpeg', output.quality);
case 'avif':
return await canvas.encode('avif', output.cfg);
return await canvas.encode('avif', output.cfg ?? { quality: 0 });
}
} catch {
throw Error(`failed to export canvas as ${output.format}`);
}
};

export const textHeight = (context: SKRSContext2D, maxWidth: number, title: string, lines?: string[]): number => {
const bottomLineSpacing = 15;
const titleMetrics = context.measureText(title);

let height = titleMetrics.actualBoundingBoxAscent + titleMetrics.actualBoundingBoxDescent;

if (lines) {
lines.forEach((line) => {
const text = wrapText(context, line, maxWidth);
text.forEach((t) => {
const metrics = context.measureText(t);
height += metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
});
});
}

return height + bottomLineSpacing;
};
8 changes: 4 additions & 4 deletions tests/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as assert from 'node:assert';
import { existsSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { readdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

import { describe, test } from 'mocha';
import { Mod } from 'warframe-items';
import type { Mod } from 'warframe-items';
import { find } from 'warframe-items/utilities';

import generate from '../src/generator.js';
import { Format } from '../src/utils.js';
import type { Format } from '../src/utils.js';

describe('Generate a mod', () => {
test('run test', async () => {
Expand Down Expand Up @@ -37,7 +37,7 @@ describe('Generate a mod', () => {
const modCanvas = await generate(mod, { format }, mod.fusionLimit);
assert.ok(modCanvas);

if (modCanvas) await writeFile(join(imagePath, `${mod.name}.${format}`), modCanvas);
if (modCanvas) await writeFile(join(imagePath, `${mod.name.replaceAll(' ', '_')}.${format}`), modCanvas);
})
);
}
Expand Down