Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
cd4754901c | |||
2a8ee97e76 | |||
64354f93f8 | |||
83b42776d1 | |||
54a405b4b9 | |||
6c3194c256 | |||
dc49eb961f | |||
055e42ecf7 | |||
4d318d20ee | |||
7a752a92cb | |||
03fff8576e | |||
6827b966fd | |||
68c4222b3b | |||
1e90062f6d | |||
ff5c543147 | |||
52db63d395 | |||
e786943d16 | |||
3adf3bf76d | |||
6213eda240 | |||
7f4268f960 | |||
a5979647fc | |||
0e9661f146 | |||
b0249f8baf | |||
9cef34b976 | |||
28f214fb51 | |||
0099624165 | |||
4e79fd38a1 | |||
8e7bcc185c | |||
1e6e336f73 | |||
f497057dae | |||
3bb0308949 | |||
0da7c3ab23 | |||
c196646955 | |||
3c3ee0565a | |||
98a2ce93fe | |||
7236e9f04e | |||
b81144153b | |||
84bc6d79f5 | |||
c698ac3499 | |||
e4484b6873 | |||
f330dbde33 | |||
bb2403adad | |||
f7e4116b42 | |||
50f27010f0 | |||
5ec05e3db7 | |||
5a837cc373 | |||
317d2e6de3 |
26
.gitignore
vendored
@ -1,23 +1,7 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
node_modules
|
||||||
|
|
||||||
# dependencies
|
# dotenv environment variable files
|
||||||
/node_modules
|
.env
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
**/dist/**/*
|
||||||
/coverage
|
static/dist
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
47
README.md
@ -1,46 +1,5 @@
|
|||||||
# Getting Started with Create React App
|
# Dominionator
|
||||||
|
|
||||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
A web-app for making [Dominion](https://www.riograndegames.com/games/dominion/) fan-cards. Inspired by [shardofhonor's dominion-card-generator](https://github.com/shardofhonor/dominion-card-generator), but trying to be more modern and maintainable with React and Typescript. Currently it's not much of a web-app since there are no inputs to edit the cards, but you can mess with the hardcoded array in `src/cards.ts` to render arbitrary cards.
|
||||||
|
|
||||||
## Available Scripts
|
To run it, clone the repo and run `deno task dev`.
|
||||||
|
|
||||||
In the project directory, you can run:
|
|
||||||
|
|
||||||
### `npm start`
|
|
||||||
|
|
||||||
Runs the app in the development mode.\
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
|
||||||
|
|
||||||
The page will reload if you make edits.\
|
|
||||||
You will also see any lint errors in the console.
|
|
||||||
|
|
||||||
### `npm test`
|
|
||||||
|
|
||||||
Launches the test runner in the interactive watch mode.\
|
|
||||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
|
||||||
|
|
||||||
### `npm run build`
|
|
||||||
|
|
||||||
Builds the app for production to the `build` folder.\
|
|
||||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
|
||||||
|
|
||||||
The build is minified and the filenames include the hashes.\
|
|
||||||
Your app is ready to be deployed!
|
|
||||||
|
|
||||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
|
||||||
|
|
||||||
### `npm run eject`
|
|
||||||
|
|
||||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
|
||||||
|
|
||||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
|
||||||
|
|
||||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
|
||||||
|
|
||||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
|
||||||
|
|
||||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
42
deno.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
29401
package-lock.json
generated
48
package.json
@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "dominionator",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.playbox.link/dylan/dominionator.git"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"@types/jest": "^27.5.2",
|
|
||||||
"@types/node": "^16.18.38",
|
|
||||||
"@types/react": "^18.2.14",
|
|
||||||
"@types/react-dom": "^18.2.7",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
1230
reference.js
Normal file
3
root.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { dirname, fromFileUrl } from "jsr:@std/path";
|
||||||
|
|
||||||
|
export const projectRootDir = dirname(fromFileUrl(import.meta.url));
|
38
src/App.css
@ -1,38 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
23
src/App.tsx
@ -1,23 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import logo from './logo.svg';
|
|
||||||
import './App.css';
|
|
||||||
import { Card } from './Card';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<Card card={{
|
|
||||||
name: "My Card",
|
|
||||||
v: 1,
|
|
||||||
types: ["Action"],
|
|
||||||
cost: "5",
|
|
||||||
preview: "",
|
|
||||||
text: "+3 Cards",
|
|
||||||
art: "",
|
|
||||||
artCredit: "",
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
43
src/Card.tsx
@ -1,43 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
type CardType =
|
|
||||||
| "Action"
|
|
||||||
| "Treasure"
|
|
||||||
| "Victory"
|
|
||||||
| "Curse"
|
|
||||||
| "Reaction"
|
|
||||||
| "Duration"
|
|
||||||
| "Reserve"
|
|
||||||
| "Night"
|
|
||||||
|
|
||||||
type CardData = {
|
|
||||||
name: string,
|
|
||||||
text: string,
|
|
||||||
cost: string,
|
|
||||||
preview: string,
|
|
||||||
types: Array<CardType>,
|
|
||||||
art: string,
|
|
||||||
artCredit: string,
|
|
||||||
v: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawCard = (canvas: HTMLCanvasElement, card: CardData) => {
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("Could not get the context of the canvas");
|
|
||||||
}
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
context.font = "88px myTitle"
|
|
||||||
context.fillText(card.name, 100, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Card = (props: {card: CardData}) => {
|
|
||||||
const {
|
|
||||||
card
|
|
||||||
} = props;
|
|
||||||
return <canvas className="card" width={1403} height={2151} ref={(el) => {
|
|
||||||
if (el) {
|
|
||||||
drawCard(el, card);
|
|
||||||
}
|
|
||||||
}}></canvas>
|
|
||||||
}
|
|
27
src/cards.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
DominionCard,
|
||||||
|
TYPE_ACTION,
|
||||||
|
// TYPE_DURATION,
|
||||||
|
// TYPE_NIGHT,
|
||||||
|
// TYPE_TREASURE,
|
||||||
|
// TYPE_VICTORY,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
const expansionIcon = "";
|
||||||
|
const author = "Dylan";
|
||||||
|
|
||||||
|
const _sampleCard: DominionCard = {
|
||||||
|
orientation: "card",
|
||||||
|
title: "Sample",
|
||||||
|
description: "",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cards: DominionCard[] = [_sampleCard, _sampleCard];
|
10
src/client/App.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { cards } from "../cards.ts";
|
||||||
|
import { Card } from "./Card.tsx";
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
return <div>
|
||||||
|
{cards.map((card) => {
|
||||||
|
return <Card key={`${card.title}`} card={card}/>
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
28
src/client/Card.tsx
Normal file
@ -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 <canvas style={{width: "2.5in"}} width={width} height={height} ref={async (canvasElement) => {
|
||||||
|
if (canvasElement) {
|
||||||
|
const context = canvasElement.getContext("2d");
|
||||||
|
if (context) {
|
||||||
|
await loadFonts();
|
||||||
|
await loadImages();
|
||||||
|
await drawCard(context, card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}></canvas>
|
||||||
|
}
|
14
src/client/index.tsx
Normal file
@ -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(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
5
src/colorhelper.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import parseColor1 from "npm:parse-color";
|
||||||
|
|
||||||
|
export const parseColor = (c: string): { rgb: [number, number, number] } => {
|
||||||
|
return parseColor1(c);
|
||||||
|
};
|
480
src/dominiontext.ts
Normal file
@ -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> = T | Promise<T>;
|
||||||
|
|
||||||
|
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<PieceMeasure>;
|
||||||
|
renderPiece: (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
) => PromiseOr<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PieceDef<T extends Piece["type"], M extends PieceMeasure> = {
|
||||||
|
type: T;
|
||||||
|
measure(
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece & { type: T },
|
||||||
|
tools: PieceTools
|
||||||
|
): PromiseOr<M>;
|
||||||
|
render(
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece & { type: T },
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
measure: NoInfer<M>,
|
||||||
|
tools: PieceTools
|
||||||
|
): PromiseOr<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pieceDef = <T extends Piece["type"], M extends PieceMeasure>(
|
||||||
|
def: PieceDef<T, M>
|
||||||
|
) => {
|
||||||
|
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;
|
||||||
|
};
|
405
src/draw.ts
Normal file
@ -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<string, HTMLImageElement> = {};
|
||||||
|
export const loadImage = (
|
||||||
|
src: string,
|
||||||
|
key?: string
|
||||||
|
): Promise<HTMLImageElement | null> => {
|
||||||
|
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<void> => {
|
||||||
|
if (card.orientation === "card") {
|
||||||
|
return drawStandardCard(context, card);
|
||||||
|
} else {
|
||||||
|
return drawLandscapeCard(context, card);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _rgbCache: Record<string, { r: number; g: number; b: number }> = {};
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
// TODO: everything
|
||||||
|
};
|
41
src/fonthelper.ts
Normal file
@ -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);
|
||||||
|
};
|
@ -1,46 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'myTitle';
|
|
||||||
font-display: auto;
|
|
||||||
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: 'myText';
|
|
||||||
font-display: auto;
|
|
||||||
src: local("Times New Roman"), serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'mySpecials';
|
|
||||||
font-display: auto;
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: 2in;
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById('root') as HTMLElement
|
|
||||||
);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
0
src/isocanvas.ts
Normal file
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
|
Before Width: | Height: | Size: 2.6 KiB |
1
src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="react-scripts" />
|
|
@ -1,15 +0,0 @@
|
|||||||
import { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
25
src/richtext.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// type RichnessNodeDefinition<N extends {type: string}> = {
|
||||||
|
// type: N["type"]
|
||||||
|
// measure(context: CanvasRenderingContext2D, node: N): Promise<TextMetrics>;
|
||||||
|
// render(
|
||||||
|
// context: CanvasRenderingContext2D,
|
||||||
|
// node: N,
|
||||||
|
// x: number,
|
||||||
|
// y: number
|
||||||
|
// ): Promise<void>;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// type Richness<N extends {type: string}> = {[K in N["type"]]: RichnessNodeDefinition<N & {type: K}>}
|
||||||
|
|
||||||
|
// const drawRichText = <N extends {type: string}>(
|
||||||
|
// context: CanvasRenderingContext2D,
|
||||||
|
// richness: Richness<N>,
|
||||||
|
// richText: N[],
|
||||||
|
// x: number,
|
||||||
|
// y: number,
|
||||||
|
// maxWidth: number,
|
||||||
|
// ) => {
|
||||||
|
// context.save();
|
||||||
|
// const
|
||||||
|
// context.restore();
|
||||||
|
// };
|
78
src/sampleData.ts
Normal file
@ -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: "",
|
||||||
|
},
|
||||||
|
];
|
14
src/server/index.ts
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
});
|
@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
171
src/types.ts
Normal file
@ -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<DominionCardType>;
|
||||||
|
image: string;
|
||||||
|
artist: string;
|
||||||
|
author: string;
|
||||||
|
version: string;
|
||||||
|
cost: DominionText;
|
||||||
|
expansionIcon: string;
|
||||||
|
preview?: DominionText;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
orientation: "landscape";
|
||||||
|
title: string;
|
||||||
|
description: DominionText;
|
||||||
|
types: Array<DominionLandscapeType>;
|
||||||
|
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<DominionCard>;
|
||||||
|
icon: string;
|
||||||
|
customSymbols: Array<DominionCustomSymbol>;
|
||||||
|
customCardTypes: Array<DominionCustomCardType>;
|
||||||
|
customLandscapeTypes: Array<DominionCustomLandscapeType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
BIN
static/assets/BaseCardBrown.png
Normal file
After Width: | Height: | Size: 624 KiB |
BIN
static/assets/BaseCardColorOne.png
Normal file
After Width: | Height: | Size: 484 KiB |
BIN
static/assets/BaseCardGray.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
static/assets/BaseCardIcon.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
static/assets/CardBrown.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
static/assets/CardColorOne.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
static/assets/CardColorThree.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
static/assets/CardColorTwo.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
static/assets/CardColorTwoBig.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
static/assets/CardColorTwoNight.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
static/assets/CardColorTwoSmall.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
static/assets/CardGray.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
static/assets/CardPortraitIcon.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
static/assets/Coin.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
static/assets/Debt.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
static/assets/DescriptionFocus.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
static/assets/DoubleColorOne.png
Normal file
After Width: | Height: | Size: 960 KiB |
BIN
static/assets/DoubleUncoloredDetails.png
Normal file
After Width: | Height: | Size: 466 KiB |
BIN
static/assets/EventBrown.png
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
static/assets/EventBrown2.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
static/assets/EventColorOne.png
Normal file
After Width: | Height: | Size: 353 KiB |
BIN
static/assets/EventColorTwo.png
Normal file
After Width: | Height: | Size: 291 KiB |
BIN
static/assets/EventHeirloom.png
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
static/assets/Heirloom.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
static/assets/MatBannerBottom.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
static/assets/MatBannerTop.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
static/assets/MatIcon.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
static/assets/PileMarkerColorOne.png
Normal file
After Width: | Height: | Size: 916 KiB |
BIN
static/assets/PileMarkerGrey.png
Normal file
After Width: | Height: | Size: 735 KiB |
BIN
static/assets/PileMarkerIcon.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
static/assets/Potion.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
static/assets/Sun.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
static/assets/TraitBrown.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
static/assets/TraitBrownSide.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
static/assets/TraitColorOne.png
Normal file
After Width: | Height: | Size: 384 KiB |
BIN
static/assets/TraitColorOneSide.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
static/assets/Traveller.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
static/assets/VP-Token.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
static/assets/VP.png
Normal file
After Width: | Height: | Size: 38 KiB |
27
static/fonts.css
Normal file
@ -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');
|
||||||
|
} */
|
13
static/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dominionator</title>
|
||||||
|
<link rel="stylesheet" href="/static/fonts.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/dist/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
tools/build.ts
Normal file
@ -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();
|
37
tools/dev.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
|