init
This commit is contained in:
commit
d8de108153
|
@ -0,0 +1,2 @@
|
|||
/result
|
||||
/node_modules/
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="1012"
|
||||
height="1404"
|
||||
viewBox="0 0 1012 1404"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
sodipodi:docname="error.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showborder="false"
|
||||
shape-rendering="auto"
|
||||
showgrid="true"
|
||||
inkscape:zoom="0.96390292"
|
||||
inkscape:cx="282.70482"
|
||||
inkscape:cy="283.74227"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1046"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="-2"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1"><inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="506"
|
||||
originy="702"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="10"
|
||||
dotted="true"
|
||||
gridanglex="30"
|
||||
gridanglez="30"
|
||||
visible="true"
|
||||
snapvisiblegridlinesonly="false"
|
||||
enabled="false" /></sodipodi:namedview><defs
|
||||
id="defs1"><rect
|
||||
x="20.555475"
|
||||
y="21.720669"
|
||||
width="947.26782"
|
||||
height="1339.3308"
|
||||
id="rect1" /></defs><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><path
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:#343434;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect1-2"
|
||||
width="1003.9467"
|
||||
height="1395.9467"
|
||||
x="4.0266695"
|
||||
y="4.0266695"
|
||||
sodipodi:type="rect"
|
||||
ry="58.536545"
|
||||
mask="none"
|
||||
inkscape:label="bg back" /><text
|
||||
xml:space="preserve"
|
||||
id="text1"
|
||||
style="font-size:40px;line-height:1;font-family:Alatsi;-inkscape-font-specification:Alatsi;letter-spacing:0px;white-space:pre;shape-inside:url(#rect1);display:inline"><tspan
|
||||
x="20.554688"
|
||||
y="57.276259"
|
||||
id="tspan4"><tspan
|
||||
style="font-family:Minecraft;-inkscape-font-specification:Minecraft;fill:#ffffff"
|
||||
id="tspan1">$error$</tspan></tspan></text></g></svg>
|
After Width: | Height: | Size: 2.7 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 876 KiB |
|
@ -0,0 +1,150 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="1012"
|
||||
height="1404"
|
||||
viewBox="0 0 1012 1404"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
sodipodi:docname="player-help.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showborder="false"
|
||||
shape-rendering="auto"
|
||||
showgrid="true"
|
||||
inkscape:zoom="0.48195146"
|
||||
inkscape:cx="401.49271"
|
||||
inkscape:cy="606.90759"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1046"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="-2"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"><inkscape:grid
|
||||
id="grid1"
|
||||
units="px"
|
||||
originx="506"
|
||||
originy="702"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="10"
|
||||
dotted="true"
|
||||
gridanglex="30"
|
||||
gridanglez="30"
|
||||
visible="true"
|
||||
snapvisiblegridlinesonly="false"
|
||||
enabled="false" /></sodipodi:namedview><defs
|
||||
id="defs1"><rect
|
||||
x="20.555475"
|
||||
y="21.720669"
|
||||
width="947.26782"
|
||||
height="1339.3308"
|
||||
id="rect1" /></defs><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><path
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:#343434;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect1-2"
|
||||
width="1003.9467"
|
||||
height="1395.9467"
|
||||
x="4.0266695"
|
||||
y="4.0266695"
|
||||
sodipodi:type="rect"
|
||||
ry="58.536545"
|
||||
mask="none"
|
||||
inkscape:label="bg back" /><text
|
||||
xml:space="preserve"
|
||||
id="text1"
|
||||
style="font-size:40px;line-height:1;font-family:Minecraft;-inkscape-font-specification:Minecraft;fill:#ffffff;letter-spacing:0px;white-space:pre;shape-inside:url(#rect1);display:inline"
|
||||
transform="translate(18.038217,310.07099)"><tspan
|
||||
x="20.554688"
|
||||
y="57.276259"
|
||||
id="tspan5"><tspan
|
||||
id="tspan1">Get your player's name, a link to their </tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="101.58182"
|
||||
id="tspan7"><tspan
|
||||
id="tspan6">profile picture (PNG preferred), and </tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="145.88737"
|
||||
id="tspan9"><tspan
|
||||
id="tspan8">the previously calculated damage and </tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="190.19293"
|
||||
id="tspan11"><tspan
|
||||
id="tspan10">health.
|
||||
</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="234.49849"
|
||||
id="tspan13"><tspan
|
||||
id="tspan12">
|
||||
</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="278.80404"
|
||||
id="tspan15"><tspan
|
||||
id="tspan14">Got all that? Apply it to the end of the </tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="323.1096"
|
||||
id="tspan17"><tspan
|
||||
id="tspan16">URL as so:
|
||||
</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="367.41517"
|
||||
id="tspan19"><tspan
|
||||
id="tspan18">
|
||||
</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="407.41516"
|
||||
id="tspan21"><tspan
|
||||
style="font-size:26.6667px"
|
||||
id="tspan20">/front.png?name=player&image=https://example.com/</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="447.41516"
|
||||
id="tspan23"><tspan
|
||||
style="font-size:26.6667px"
|
||||
id="tspan22">image.png&dmg=0&hp=0
|
||||
</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="487.41516"
|
||||
id="tspan25"><tspan
|
||||
style="font-size:26.6667px"
|
||||
id="tspan24">
|
||||
</tspan></tspan><tspan
|
||||
x="20.554688"
|
||||
y="531.72072"
|
||||
id="tspan26">If you're happy with how that looks, </tspan><tspan
|
||||
x="20.554688"
|
||||
y="576.02626"
|
||||
id="tspan27">create a new custom card. Set its </tspan><tspan
|
||||
x="20.554688"
|
||||
y="620.3318"
|
||||
id="tspan28">front texture URL to this, and its back </tspan><tspan
|
||||
x="20.554688"
|
||||
y="664.63734"
|
||||
id="tspan29">texture URL to /back.png.
|
||||
</tspan><tspan
|
||||
x="20.554688"
|
||||
y="708.94288"
|
||||
id="tspan30">
|
||||
</tspan><tspan
|
||||
x="20.554688"
|
||||
y="753.24843"
|
||||
id="tspan31">After you're done, continue as normal.</tspan></text></g></svg>
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709569716,
|
||||
"narHash": "sha256-iOR44RU4jQ+YPGrn+uQeYAp7Xo7Z/+gT+wXJoGxxLTY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "617579a787259b9a6419492eaac670a5f7663917",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
description = "cardgen";
|
||||
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
package = builtins.fromJSON (builtins.readFile ./package.json);
|
||||
in
|
||||
rec {
|
||||
packages = flake-utils.lib.flattenTree rec {
|
||||
cardgen = pkgs.buildNpmPackage rec {
|
||||
pname = "cardgen";
|
||||
inherit (package) version;
|
||||
|
||||
npmDepsHash = "sha256-1gQydlNbMBwCWj8++d79aBy5QaIIwiLMpsdurRz8FDA=";
|
||||
|
||||
doCheck = false;
|
||||
|
||||
nativeBuildInputs = with pkgs; [ makeWrapper ];
|
||||
|
||||
dontNpmBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
mv ./* $out/
|
||||
makeWrapper ${pkgs.nodejs-slim}/bin/node $out/bin/cardgen \
|
||||
--add-flags $out/index.js \
|
||||
--chdir $out/
|
||||
'';
|
||||
|
||||
src = ./.;
|
||||
};
|
||||
};
|
||||
|
||||
defaultPackage = packages.cardgen;
|
||||
}) // {
|
||||
nixosModules = {
|
||||
cardgen = { config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.cardgen;
|
||||
in {
|
||||
options.services.cardgen = {
|
||||
enable = mkEnableOption "Enables the cardgen server";
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3500;
|
||||
};
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = self.defaultPackage.${pkgs.system};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services."cardgen" = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
PORT = toString cfg.port;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
ExecStart = "${getExe cfg.package}";
|
||||
DynamicUser = "yes";
|
||||
StateDirectory = "cardgen";
|
||||
StateDirectoryMode = "0755";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
uses the following fonts:
|
||||
- [Minecraft](https://github.com/IdreesInc/Minecraft-Font/releases/tag/v1.0)
|
||||
- [Atkinson Hyperlegible](https://brailleinstitute.org/freefont)
|
|
@ -0,0 +1,174 @@
|
|||
import express from 'express';
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
import * as fs from 'fs/promises';
|
||||
import { escape } from 'html-escaper';
|
||||
import { request } from 'undici';
|
||||
import { lookup as mimeLookup } from 'mime-types';
|
||||
|
||||
const errorSVG = 'cards/error.svg';
|
||||
|
||||
const cards = {
|
||||
player: {
|
||||
cards: {
|
||||
front: {
|
||||
name: 'cards/player-front.svg',
|
||||
parameters: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'dmg', type: 'number' },
|
||||
{ name: 'hp', type: 'number' },
|
||||
{ name: 'image', type: 'image', optional: true },
|
||||
],
|
||||
},
|
||||
back: {
|
||||
name: 'cards/skilled-back.png',
|
||||
ignoreParameters: true,
|
||||
}
|
||||
},
|
||||
special: {
|
||||
help: 'cards/player-help.svg',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function svg(filename, params) {
|
||||
let svg = await fs.readFile(filename, 'utf8');
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
svg = svg.replaceAll('$' + key + '$', escape(value));
|
||||
}
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
async function render(svg) {
|
||||
const opts = {
|
||||
font: {
|
||||
fontFiles: [
|
||||
'fonts/Atkinson-Hyperlegible-Bold.otf',
|
||||
'fonts/Atkinson-Hyperlegible-BoldItalic.otf',
|
||||
'fonts/Atkinson-Hyperlegible-Italic.otf',
|
||||
'fonts/Atkinson-Hyperlegible-Regular.otf',
|
||||
'fonts/Minecraft.otf',
|
||||
'fonts/Minecraft-Bold.otf',
|
||||
],
|
||||
loadSystemFonts: false, // It will be faster to disable loading system fonts.
|
||||
defaultFontFamily: 'Atkinson Hyperlegible',
|
||||
},
|
||||
};
|
||||
|
||||
const resvg = new Resvg(svg, opts);
|
||||
const pngData = resvg.render();
|
||||
const pngBuffer = pngData.asPng();
|
||||
|
||||
return pngBuffer;
|
||||
}
|
||||
|
||||
|
||||
const server = express();
|
||||
|
||||
async function returns(req, res, filename, params) {
|
||||
const ext = req.params.ext;
|
||||
|
||||
const s = await svg(filename, params);
|
||||
if (ext === 'png') {
|
||||
const r = await render(s);
|
||||
res.type('png');
|
||||
res.setHeader('Content-Length', r.byteLength);
|
||||
res.send(r);
|
||||
} else if (ext === 'svg') {
|
||||
res.type('svg');
|
||||
res.setHeader('Content-Length', s.length);
|
||||
res.send(s);
|
||||
} else {
|
||||
res.status(400).send(`Unknown filetype '${ext}'`);
|
||||
}
|
||||
}
|
||||
|
||||
async function err(req, res, error) {
|
||||
returns(req, res, errorSVG, { error });
|
||||
}
|
||||
|
||||
const userAgent = 'cardgen/0.0.0 (https://oat.zone/)';
|
||||
|
||||
async function resolveImg(url) {
|
||||
const { body, statusCode } = await request(url, {
|
||||
headers: { 'user-agent': userAgent },
|
||||
});
|
||||
if (statusCode !== 200) throw `Image returned non-200 error code`;
|
||||
|
||||
const blob = await body.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const b64 = Buffer.from(arrayBuffer).toString('base64');
|
||||
|
||||
const mime = mimeLookup(url) || 'image/png';
|
||||
|
||||
return 'data:' + mime + ';base64,' + b64;
|
||||
}
|
||||
|
||||
function resolveNumber(str) {
|
||||
const int = parseInt(str);
|
||||
if (isNaN(int)) throw 'Invalid number provided';
|
||||
return int;
|
||||
}
|
||||
|
||||
server.get('/:type/:name.:ext', async (req, res) => {
|
||||
if (req.headers['user-agent'] === userAgent) return err(req, res, 'Stop that.');
|
||||
|
||||
const type = req.params.type;
|
||||
const name = req.params.name;
|
||||
const ext = req.params.ext;
|
||||
|
||||
const card = cards[type];
|
||||
|
||||
if (!card) return err(req, res, `No card named ${type} found!`);
|
||||
|
||||
const tex = card.cards[name];
|
||||
|
||||
if (!tex) return err(req, res, `Card ${type} does not have ${name}`);
|
||||
|
||||
let params = {};
|
||||
|
||||
if (card.special.help && Object.entries(req.query).length === 0 && tex.parameters)
|
||||
return returns(req, res, card.special.help, {});
|
||||
|
||||
if (tex.name.endsWith('.png')) {
|
||||
if (ext !== 'png') {
|
||||
res.status(400).send(`Cannot offer ${ext}; only a png of this texture xists`);
|
||||
}
|
||||
return res.sendFile(tex.name, { root: './' });
|
||||
}
|
||||
|
||||
for (const param of (tex.parameters || [])) {
|
||||
const name = param.name;
|
||||
const type = param.type;
|
||||
const isOptional = param.optional;
|
||||
|
||||
const value = req.query[name];
|
||||
|
||||
if (!isOptional && (value === undefined || value === null || value === '')) {
|
||||
return err(req, res, `Required argument ${name} missing`);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
params[name] = value || '';
|
||||
break;
|
||||
case 'number':
|
||||
params[name] = value ? resolveNumber(value) : 0;
|
||||
break;
|
||||
case 'image':
|
||||
params[name] = value ? await resolveImg(value) : '';
|
||||
break;
|
||||
}
|
||||
} catch(e) {
|
||||
return err(req, res, `${name}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
await returns(req, res, tex.name, params);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 8080;
|
||||
server.listen(port);
|
||||
console.log(`listening on port ${port}`);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "cardgen",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"author": "oatmealine",
|
||||
"license": "GPL-3.0-only",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"express": "^4.18.3",
|
||||
"html-escaper": "^3.0.3",
|
||||
"mime-types": "^2.1.35",
|
||||
"undici": "^6.7.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue