Compare commits
57 commits
Author | SHA1 | Date | |
---|---|---|---|
c89080ff00 | |||
f24047b33b | |||
f004daaa3c | |||
ff39c8eeb3 | |||
cf19301f96 | |||
a5068e4b64 | |||
04ba5b7e4e | |||
546102205e | |||
fd06a2a618 | |||
e391b9b6bf | |||
bfe739a487 | |||
85881f877c | |||
4d8b0b85dd | |||
33603eabff | |||
2647238410 | |||
405fd3cad5 | |||
9a13c3c393 | |||
1e1faaa4c9 | |||
0edc9a7ab4 | |||
8915a36b3d | |||
d443fc6d7e | |||
6091c19c8b | |||
b74d5838eb | |||
918e021eed | |||
8ea2782038 | |||
7e41fe054e | |||
7e6a761363 | |||
7919e4fb7d | |||
e82fcd5d42 | |||
c89777621a | |||
3409978203 | |||
d8dff75411 | |||
944dc9ca5f | |||
26ceccbde2 | |||
e34c3a96c4 | |||
86c9644972 | |||
e9bc9f568d | |||
eb455e2311 | |||
57f32ef643 | |||
cd76a36d99 | |||
71af6bb680 | |||
6720eace7a | |||
0982857082 | |||
1ba396bc7c | |||
a656d37d8c | |||
14efcf03a1 | |||
b5a6b70c2b | |||
5734774366 | |||
054f9a9218 | |||
79f9108f34 | |||
de53e1b741 | |||
d319de86d9 | |||
f54b61e236 | |||
c481d5739f | |||
89091dcd24 | |||
64fa7098a0 | |||
7cf393af71 |
24 changed files with 1846 additions and 337 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
.npm
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
@ -20,3 +21,6 @@ pnpm-debug.log*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
/kollagen
|
||||||
|
|
34
.gitlab-ci.yml
Normal file
34
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
|
||||||
|
deno_lint:
|
||||||
|
stage: lint
|
||||||
|
image: denoland/deno:latest
|
||||||
|
script:
|
||||||
|
- cd cli
|
||||||
|
- deno lint
|
||||||
|
|
||||||
|
deno_build:
|
||||||
|
stage: build
|
||||||
|
image: denoland/deno:latest
|
||||||
|
script:
|
||||||
|
- cd cli
|
||||||
|
- deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- kollagen
|
||||||
|
|
||||||
|
app_build:
|
||||||
|
stage: build
|
||||||
|
image: node:lts
|
||||||
|
script:
|
||||||
|
- node --version && npm --version
|
||||||
|
- make app
|
||||||
|
cache:
|
||||||
|
key: ${CI_COMMIT_REF_SLUG}
|
||||||
|
paths:
|
||||||
|
- .npm
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- dist
|
49
CHANGELOG.md
Normal file
49
CHANGELOG.md
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.0] - 2020-07-20
|
||||||
|
### Added
|
||||||
|
- Specific modes can now be excluded from rotation in shuffle & recursive modes
|
||||||
|
|
||||||
|
## [0.1.0] - 2020-07-17
|
||||||
|
### Added
|
||||||
|
- Recursive collage placement mode!
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored modes - they are no longer solely responsible for drawing, but can use common rendering functions through composition.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Incorrect size calculation for "Irregular column" mode.
|
||||||
|
|
||||||
|
## [0.0.4] - 2020-07-17
|
||||||
|
### Fixed
|
||||||
|
- Blending mode now properly resets after using the "Blend" collage mode.
|
||||||
|
|
||||||
|
## [0.0.3] - 2020-07-17
|
||||||
|
### Added
|
||||||
|
- "Blend" collage mode - based on a random selection of compositing operations.
|
||||||
|
- LICENSE file.
|
||||||
|
|
||||||
|
## [0.0.2] - 2020-07-16
|
||||||
|
### Changed
|
||||||
|
- Split "Grid" mode into "Clean grid" and "Irregular grid" (because "clean cropping" config only really applied to this mode.)
|
||||||
|
|
||||||
|
## [0.0.1] - 2020-07-15
|
||||||
|
### Added
|
||||||
|
- First public version!
|
||||||
|
- Basic modes: Grid, (ir)regular row, (ir)regular column, concentric modes.
|
||||||
|
|
||||||
|
|
||||||
|
[Unreleased]: https://gitlab.com/tmladek/kollagen/-/compare/v0.2.0...master
|
||||||
|
[0.2.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.1.0...v0.2.0
|
||||||
|
[0.1.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.4...v0.1.0
|
||||||
|
[0.0.4]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.3...v0.0.4
|
||||||
|
[0.0.3]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.2...v0.0.3
|
||||||
|
[0.0.2]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.1...v0.0.2
|
||||||
|
[0.0.1]: https://gitlab.com/tmladek/kollagen/-/tags/v0.0.1
|
||||||
|
|
21
Makefile
Normal file
21
Makefile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
all: clean deno_lint deno_docker tgbot app
|
||||||
|
|
||||||
|
deno_lint:
|
||||||
|
cd cli && deno lint
|
||||||
|
|
||||||
|
deno: ./kollagen
|
||||||
|
|
||||||
|
./kollagen:
|
||||||
|
cd cli && deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
|
||||||
|
|
||||||
|
deno_docker:
|
||||||
|
docker run --rm -v $$PWD:/app denoland/deno compile --import-map /app/cli/import_map.json --allow-read --allow-write --unstable -o /app/kollagen /app/cli/main.ts
|
||||||
|
|
||||||
|
app:
|
||||||
|
npm ci --cache .npm --prefer-offline && npm run build
|
||||||
|
|
||||||
|
tgbot: ./kollagen
|
||||||
|
docker build -t kollagen-bot -f tgbot/Dockerfile .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rvf dist kollagen
|
96
cli/collages.ts
Normal file
96
cli/collages.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { CollageModes } from "../src/common/collages.ts";
|
||||||
|
import {
|
||||||
|
init,
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
EmulatedCanvas2D,
|
||||||
|
Image,
|
||||||
|
} from "https://deno.land/x/canvas@v1.3.0/mod.ts";
|
||||||
|
import { CollageImage } from "../src/common/types.ts";
|
||||||
|
|
||||||
|
const canvasKit = await init();
|
||||||
|
|
||||||
|
export class ProxyImage implements CollageImage {
|
||||||
|
private filepath: string | null;
|
||||||
|
private _image: Image | undefined;
|
||||||
|
|
||||||
|
constructor(input: string | Image) {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
this.filepath = input;
|
||||||
|
} else {
|
||||||
|
this.filepath = null;
|
||||||
|
this._image = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get path(): string | null {
|
||||||
|
return this.filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get image(): Image {
|
||||||
|
if (!this._image) {
|
||||||
|
const image = canvasKit.MakeImageFromEncoded(
|
||||||
|
Deno.readFileSync(this.filepath!)
|
||||||
|
);
|
||||||
|
if (!image) {
|
||||||
|
throw Error(`Failed loading ${this.filepath}!`);
|
||||||
|
}
|
||||||
|
this._image = image;
|
||||||
|
}
|
||||||
|
return this._image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get width(): number {
|
||||||
|
return this.image.width();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get height(): number {
|
||||||
|
return this.image.height();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "https://deno.land/x/canvas@v1.3.0/mod.ts" {
|
||||||
|
interface HTMLCanvasElement {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
getContext(x: "2d"): CanvasRenderingContext2D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DenoCollageModes extends CollageModes<
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
ProxyImage,
|
||||||
|
EmulatedCanvas2D
|
||||||
|
> {
|
||||||
|
createCanvas(w: number, h: number): EmulatedCanvas2D {
|
||||||
|
const canvas = canvasKit.MakeCanvas(Math.round(w), Math.round(h));
|
||||||
|
if (!canvas) {
|
||||||
|
throw Error("Error initializing canvas.");
|
||||||
|
}
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasToImage(canvas: EmulatedCanvas2D): PromiseLike<ProxyImage> {
|
||||||
|
const image = canvasKit.MakeImageFromEncoded(canvas.toBuffer());
|
||||||
|
if (!image) {
|
||||||
|
throw Error("Something went wrong converting canvas to image.");
|
||||||
|
}
|
||||||
|
return Promise.resolve(new ProxyImage(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
drawImage(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
image: ProxyImage,
|
||||||
|
sx: number,
|
||||||
|
sy: number,
|
||||||
|
sw: number,
|
||||||
|
sh: number,
|
||||||
|
dx: number,
|
||||||
|
dy: number,
|
||||||
|
dw: number,
|
||||||
|
dh: number
|
||||||
|
): void {
|
||||||
|
ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const denoCollageModes = new DenoCollageModes();
|
6
cli/import_map.json
Normal file
6
cli/import_map.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@/common/types": "../src/common/types.ts",
|
||||||
|
"@/common/utils": "../src/common/utils.ts"
|
||||||
|
}
|
||||||
|
}
|
105
cli/main.ts
Normal file
105
cli/main.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts";
|
||||||
|
import { createCanvas } from "https://deno.land/x/canvas@v1.3.0/mod.ts";
|
||||||
|
import {
|
||||||
|
collageModeType,
|
||||||
|
DisplayCollageModeType,
|
||||||
|
displayCollageModeType,
|
||||||
|
} from "../src/common/collages.ts";
|
||||||
|
import { denoCollageModes, ProxyImage } from "./collages.ts";
|
||||||
|
import { CollageConfig } from "../src/common/types.ts";
|
||||||
|
import { choice, shuffle } from "../src/common/utils.ts";
|
||||||
|
import { walkSync } from "https://deno.land/std@0.107.0/fs/mod.ts";
|
||||||
|
import { parseCollageModes, parseDisplayCollageModes } from "./util.ts";
|
||||||
|
|
||||||
|
const args = parse(Deno.args, {
|
||||||
|
alias: {
|
||||||
|
w: "width",
|
||||||
|
h: "height",
|
||||||
|
o: "output",
|
||||||
|
m: "mode",
|
||||||
|
},
|
||||||
|
boolean: ["rr"],
|
||||||
|
default: {
|
||||||
|
w: 640,
|
||||||
|
h: 640,
|
||||||
|
include: "*.png, *.jpg",
|
||||||
|
output: "collage.png",
|
||||||
|
rl: 2,
|
||||||
|
rr: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args["mode"] === true) {
|
||||||
|
console.log(displayCollageModeType.join(", "));
|
||||||
|
Deno.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: Set<string> = new Set();
|
||||||
|
const includeExtensions = Array.from(
|
||||||
|
String(args["include"]).matchAll(/\*\.([\w]+)/g)
|
||||||
|
).map(([_, group]) => group);
|
||||||
|
|
||||||
|
args["_"].forEach((arg) => {
|
||||||
|
arg = arg.toString();
|
||||||
|
if (Deno.statSync(arg).isDirectory) {
|
||||||
|
Array.from(
|
||||||
|
walkSync(arg, {
|
||||||
|
maxDepth: Infinity,
|
||||||
|
includeDirs: false,
|
||||||
|
includeFiles: true,
|
||||||
|
exts: includeExtensions.length ? includeExtensions : undefined,
|
||||||
|
})
|
||||||
|
).forEach((entry) => files.add(entry.path));
|
||||||
|
} else {
|
||||||
|
files.add(arg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.size < 2) {
|
||||||
|
console.error("kollagen needs at least 2 images to work.");
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffledFiles = shuffle(Array.from(files));
|
||||||
|
|
||||||
|
const images: ProxyImage[] = shuffledFiles.map((file) => new ProxyImage(file));
|
||||||
|
|
||||||
|
const allModeKeys = args["mode"]
|
||||||
|
? parseDisplayCollageModes(args["mode"])
|
||||||
|
: displayCollageModeType;
|
||||||
|
|
||||||
|
const canvas = createCanvas(args["width"], args["height"]);
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const collageConfig: CollageConfig = {
|
||||||
|
numImages: args["n"],
|
||||||
|
};
|
||||||
|
const modeKey: DisplayCollageModeType = choice(allModeKeys);
|
||||||
|
|
||||||
|
if (modeKey === "recursive") {
|
||||||
|
console.log(
|
||||||
|
`Creating a recursive collage, choosing from ${shuffledFiles.length} files...`
|
||||||
|
);
|
||||||
|
await denoCollageModes.recursiveDraw(context, images, {
|
||||||
|
modes: args["rm"] ? parseCollageModes(args["rm"]) : collageModeType,
|
||||||
|
repeat: args["rr"],
|
||||||
|
level: args["rl"],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const mode = denoCollageModes.modes[modeKey];
|
||||||
|
console.log(
|
||||||
|
`Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`
|
||||||
|
);
|
||||||
|
const segments = mode.getSegments(context, collageConfig, images);
|
||||||
|
mode.place(context, images, segments);
|
||||||
|
console.log(
|
||||||
|
`Used: ${images
|
||||||
|
.slice(0, segments.length)
|
||||||
|
.map((img) => img.path)
|
||||||
|
.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = args["output"];
|
||||||
|
console.log(`Saving to "${output}"...`);
|
||||||
|
await Deno.writeFile(output, canvas.toBuffer());
|
29
cli/util.ts
Normal file
29
cli/util.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { CollageModeType, DisplayCollageModeType, isCollageModeType, isDisplayCollageModeType } from "../src/common/collages.ts";
|
||||||
|
|
||||||
|
export function parseDisplayCollageModes(input: string): DisplayCollageModeType[] {
|
||||||
|
const result: DisplayCollageModeType[] = [];
|
||||||
|
|
||||||
|
input.split(",").map((m) => m.trim()).forEach((m) => {
|
||||||
|
if (isDisplayCollageModeType(m)) {
|
||||||
|
result.push(m);
|
||||||
|
} else {
|
||||||
|
throw Error(`"${m}" is not a valid collage mode.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCollageModes(input: string): CollageModeType[] {
|
||||||
|
const result: CollageModeType[] = [];
|
||||||
|
|
||||||
|
input.split(",").map((m) => m.trim()).forEach((m) => {
|
||||||
|
if (isCollageModeType(m)) {
|
||||||
|
result.push(m);
|
||||||
|
} else {
|
||||||
|
throw Error(`"${m}" is not a valid collage mode.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "kollagen",
|
"name": "kollagen",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -2810,9 +2810,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001085",
|
"version": "1.0.30001257",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz",
|
||||||
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA==",
|
"integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"case-sensitive-paths-webpack-plugin": {
|
"case-sensitive-paths-webpack-plugin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "kollagen",
|
"name": "kollagen",
|
||||||
"version": "0.0.4",
|
"version": "0.2.0",
|
||||||
"homepage": "https://gitlab.com/tmladek/kollagen",
|
"homepage": "https://gitlab.com/tmladek/kollagen",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
199
src/collages.ts
199
src/collages.ts
|
@ -1,193 +1,14 @@
|
||||||
import {CollageMode} from "@/types";
|
import { CollageModes } from "./common/collages";
|
||||||
import {choice, randint, shuffle} from "@/utils";
|
|
||||||
|
|
||||||
const collageModeType = [
|
export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap, any> {
|
||||||
"clean_grid", "chaos_grid",
|
drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
|
||||||
"row", "irow",
|
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
|
||||||
"col", "icol",
|
|
||||||
"concentric_factor", "concentric_spaced",
|
|
||||||
"blend"
|
|
||||||
] as const;
|
|
||||||
export type CollageModeType = typeof collageModeType[number];
|
|
||||||
|
|
||||||
function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
|
|
||||||
x: number, y: number, w: number, h: number) {
|
|
||||||
const scaleRatio = Math.max(w / image.width, h / image.height);
|
|
||||||
ctx.drawImage(
|
|
||||||
image,
|
|
||||||
image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2,
|
|
||||||
w / scaleRatio, h / scaleRatio,
|
|
||||||
x - w / 2, y - h / 2,
|
|
||||||
w, h
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGridPoints(ctx: CanvasRenderingContext2D, idx: number) {
|
|
||||||
let x!: number, y!: number;
|
|
||||||
switch (idx) {
|
|
||||||
case 0:
|
|
||||||
x = ctx.canvas.width * .25;
|
|
||||||
y = ctx.canvas.height * .25;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
x = ctx.canvas.width * .75;
|
|
||||||
y = ctx.canvas.height * .25;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
x = ctx.canvas.width * .25;
|
|
||||||
y = ctx.canvas.height * .75;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
x = ctx.canvas.width * .75;
|
|
||||||
y = ctx.canvas.height * .75;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return [x, y];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modes: { [key in CollageModeType]: CollageMode } = {
|
createCanvas(w: number, h: number) {
|
||||||
"clean_grid": {
|
throw new Error("Method not implemented.");
|
||||||
name: "Clean Grid",
|
|
||||||
minImages: 4,
|
|
||||||
forceConfig: {
|
|
||||||
numImages: 4
|
|
||||||
},
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
|
||||||
const selectedImages = shuffle(images).slice(0, 4);
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const [x, y] = getGridPoints(ctx, idx);
|
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chaos_grid": {
|
|
||||||
name: "Irregular Grid",
|
|
||||||
minImages: 4,
|
|
||||||
forceConfig: {
|
|
||||||
numImages: 4
|
|
||||||
},
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
|
||||||
const selectedImages = shuffle(images).slice(0, 4);
|
|
||||||
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number]))
|
|
||||||
.forEach(([image, idx]) => {
|
|
||||||
const [x, y] = getGridPoints(ctx, idx);
|
|
||||||
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height);
|
|
||||||
ctx.drawImage(image,
|
|
||||||
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2),
|
|
||||||
image.width * scaleRatio, image.height * scaleRatio);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"row": {
|
|
||||||
name: "Regular Row",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
|
||||||
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
|
|
||||||
const y = quadrantSize[1] / 2;
|
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"irow": {
|
|
||||||
name: "Irregular Row",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
|
||||||
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
|
|
||||||
const y = quadrantSize[1] / 2;
|
|
||||||
const w = Math.min(ctx.canvas.width / 2,
|
|
||||||
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width));
|
|
||||||
cleanDraw(ctx, image, x, y, w, quadrantSize[1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"col": {
|
|
||||||
name: "Regular Column",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
|
||||||
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const x = quadrantSize[0] / 2;
|
|
||||||
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
|
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icol": {
|
|
||||||
name: "Irregular Column",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
|
||||||
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const x = quadrantSize[0] / 2;
|
|
||||||
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
|
|
||||||
const h = Math.min(ctx.canvas.height / 2,
|
|
||||||
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height));
|
|
||||||
cleanDraw(ctx, image, x, y, quadrantSize[0], h);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"concentric_factor": {
|
|
||||||
name: "Constant factor concentric",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
|
||||||
const x = ctx.canvas.width / 2;
|
|
||||||
const y = ctx.canvas.height / 2;
|
|
||||||
|
|
||||||
let factor: number;
|
|
||||||
if (Math.random() > .5) {
|
|
||||||
factor = choice([1 / Math.sqrt(2), .5, .88]);
|
|
||||||
} else {
|
|
||||||
factor = 1 - (1 / selectedImages.length);
|
|
||||||
}
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
const ratio = Math.pow(factor, idx);
|
|
||||||
cleanDraw(ctx, image, x, y, ctx.canvas.width * ratio, ctx.canvas.height * ratio);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"concentric_spaced": {
|
|
||||||
name: "Equally spaced concentric",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
|
|
||||||
|
|
||||||
selectedImages.forEach((image, idx) => {
|
|
||||||
cleanDraw(
|
|
||||||
ctx, image,
|
|
||||||
ctx.canvas.width / 2,
|
|
||||||
ctx.canvas.height / 2,
|
|
||||||
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx),
|
|
||||||
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"blend": {
|
|
||||||
name: "Blending",
|
|
||||||
minImages: 2,
|
|
||||||
place: (ctx, images, config) => {
|
|
||||||
const selectedImages = shuffle(images).slice(0, config.numImages || randint(2) + 2);
|
|
||||||
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
|
|
||||||
selectedImages.forEach((image) => {
|
|
||||||
cleanDraw(
|
|
||||||
ctx, image,
|
|
||||||
ctx.canvas.width / 2, ctx.canvas.height / 2,
|
|
||||||
ctx.canvas.width, ctx.canvas.height
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
canvasToImage(canvas: any): PromiseLike<ImageBitmap> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
export default modes;
|
}
|
||||||
|
}
|
397
src/common/collages.ts
Normal file
397
src/common/collages.ts
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
import {
|
||||||
|
CollageCanvas,
|
||||||
|
CollageConfig,
|
||||||
|
CollageContext,
|
||||||
|
CollageImage,
|
||||||
|
CollageMode,
|
||||||
|
Segment,
|
||||||
|
} from "@/common/types";
|
||||||
|
import { choice, randint, range, shuffle } from "@/common/utils";
|
||||||
|
|
||||||
|
export const collageModeType = [
|
||||||
|
"clean_grid",
|
||||||
|
"chaos_grid",
|
||||||
|
"row",
|
||||||
|
"irow",
|
||||||
|
"col",
|
||||||
|
"icol",
|
||||||
|
"concentric_factor",
|
||||||
|
"concentric_spaced",
|
||||||
|
"blend",
|
||||||
|
] as const;
|
||||||
|
export type CollageModeType = typeof collageModeType[number];
|
||||||
|
export function isCollageModeType(value: string): value is CollageModeType {
|
||||||
|
return (collageModeType as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayCollageModeType = [
|
||||||
|
...collageModeType,
|
||||||
|
"recursive",
|
||||||
|
] as const;
|
||||||
|
export type DisplayCollageModeType = typeof displayCollageModeType[number];
|
||||||
|
export function isDisplayCollageModeType(
|
||||||
|
value: string
|
||||||
|
): value is DisplayCollageModeType {
|
||||||
|
return (displayCollageModeType as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecursiveCollageConfig {
|
||||||
|
level: number;
|
||||||
|
repeat: boolean;
|
||||||
|
modes: readonly CollageModeType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class CollageModes<
|
||||||
|
C extends CollageContext,
|
||||||
|
I extends CollageImage,
|
||||||
|
CS extends CollageCanvas
|
||||||
|
> {
|
||||||
|
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
|
||||||
|
clean_grid: {
|
||||||
|
name: "Clean Grid",
|
||||||
|
minImages: 4,
|
||||||
|
forceConfig: {
|
||||||
|
numImages: 4,
|
||||||
|
},
|
||||||
|
getSegments: this.getGridSegments,
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
chaos_grid: {
|
||||||
|
name: "Irregular Grid",
|
||||||
|
minImages: 4,
|
||||||
|
forceConfig: {
|
||||||
|
numImages: 4,
|
||||||
|
},
|
||||||
|
getSegments: this.getGridSegments,
|
||||||
|
place: (ctx, images, segments) => {
|
||||||
|
const shuffledImages = shuffle(images);
|
||||||
|
shuffle(
|
||||||
|
segments.map((segment, idx) => [segment, idx] as [Segment, number])
|
||||||
|
).forEach(([segment, idx]) => {
|
||||||
|
const image = shuffledImages[idx];
|
||||||
|
const scaleRatio = Math.max(
|
||||||
|
segment.w / image.width,
|
||||||
|
segment.h / image.height
|
||||||
|
);
|
||||||
|
this.drawImage(
|
||||||
|
ctx,
|
||||||
|
image,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
segment.x - (image.width * scaleRatio) / 2,
|
||||||
|
segment.y - (image.height * scaleRatio) / 2,
|
||||||
|
image.width * scaleRatio,
|
||||||
|
image.height * scaleRatio
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
name: "Regular Row",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(4) + 2
|
||||||
|
);
|
||||||
|
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
|
return {
|
||||||
|
x: idx * segmentSize[0] + segmentSize[0] / 2,
|
||||||
|
y: segmentSize[1] / 2,
|
||||||
|
w: segmentSize[0],
|
||||||
|
h: segmentSize[1],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
irow: {
|
||||||
|
name: "Irregular Row",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(4) + 2
|
||||||
|
);
|
||||||
|
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
|
const irregularWidth = images
|
||||||
|
? segmentSize[0] +
|
||||||
|
Math.random() *
|
||||||
|
((segmentSize[1] / images[idx].height) * images[idx].width -
|
||||||
|
segmentSize[0])
|
||||||
|
: segmentSize[0] + Math.random() * segmentSize[0] * 0.5;
|
||||||
|
return {
|
||||||
|
x: idx * segmentSize[0] + segmentSize[0] / 2,
|
||||||
|
y: segmentSize[1] / 2,
|
||||||
|
w: Math.min(ctx.canvas.width / 2, irregularWidth),
|
||||||
|
h: segmentSize[1],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
name: "Regular Column",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(4) + 2
|
||||||
|
);
|
||||||
|
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
|
return {
|
||||||
|
x: segmentSize[0] / 2,
|
||||||
|
y: idx * segmentSize[1] + segmentSize[1] / 2,
|
||||||
|
w: segmentSize[0],
|
||||||
|
h: segmentSize[1],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
icol: {
|
||||||
|
name: "Irregular Column",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(4) + 2
|
||||||
|
);
|
||||||
|
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
|
const irregularHeight = images
|
||||||
|
? segmentSize[1] +
|
||||||
|
Math.random() *
|
||||||
|
((segmentSize[0] / images[idx].width) * images[idx].height -
|
||||||
|
segmentSize[1])
|
||||||
|
: segmentSize[1] + Math.random() * segmentSize[1] * 0.5;
|
||||||
|
return {
|
||||||
|
x: segmentSize[0] / 2,
|
||||||
|
y: idx * segmentSize[1] + segmentSize[1] / 2,
|
||||||
|
w: segmentSize[0],
|
||||||
|
h: Math.min(ctx.canvas.height / 2, irregularHeight),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
concentric_factor: {
|
||||||
|
name: "Constant factor concentric",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(4) + 2
|
||||||
|
);
|
||||||
|
let factor: number;
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
factor = choice([1 / Math.sqrt(2), 0.5, 0.88]);
|
||||||
|
} else {
|
||||||
|
factor = 1 - 1 / numImages;
|
||||||
|
}
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
|
const ratio = Math.pow(factor, idx);
|
||||||
|
return {
|
||||||
|
x: ctx.canvas.width / 2,
|
||||||
|
y: ctx.canvas.height / 2,
|
||||||
|
w: ctx.canvas.width * ratio,
|
||||||
|
h: ctx.canvas.height * ratio,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
concentric_spaced: {
|
||||||
|
name: "Equally spaced concentric",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(2) + 2
|
||||||
|
);
|
||||||
|
return range(numImages).map((idx) => {
|
||||||
|
return {
|
||||||
|
x: ctx.canvas.width / 2,
|
||||||
|
y: ctx.canvas.height / 2,
|
||||||
|
w: ctx.canvas.width - (ctx.canvas.width / numImages) * idx,
|
||||||
|
h: ctx.canvas.height - (ctx.canvas.height / numImages) * idx,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: this.cleanPlace.bind(this),
|
||||||
|
},
|
||||||
|
blend: {
|
||||||
|
name: "Blending",
|
||||||
|
minImages: 2,
|
||||||
|
getSegments: (ctx, config, images) => {
|
||||||
|
const numImages = Math.min(
|
||||||
|
images?.length || Infinity,
|
||||||
|
config?.numImages || randint(2) + 2
|
||||||
|
);
|
||||||
|
return range(numImages).map((_) => {
|
||||||
|
return {
|
||||||
|
x: ctx.canvas.width / 2,
|
||||||
|
y: ctx.canvas.height / 2,
|
||||||
|
w: ctx.canvas.width,
|
||||||
|
h: ctx.canvas.height,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
place: (ctx, images, segments) => {
|
||||||
|
ctx.globalCompositeOperation = choice([
|
||||||
|
"difference",
|
||||||
|
"saturation",
|
||||||
|
"soft-light",
|
||||||
|
"overlay",
|
||||||
|
]);
|
||||||
|
this.cleanPlace(ctx, images, segments);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public async recursiveDraw(
|
||||||
|
context: C,
|
||||||
|
images: I[],
|
||||||
|
recursiveConfig: RecursiveCollageConfig
|
||||||
|
) {
|
||||||
|
const localImages = images.concat();
|
||||||
|
const rootSegment: Segment = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: context.canvas.width,
|
||||||
|
h: context.canvas.height,
|
||||||
|
};
|
||||||
|
const processSegment = async (
|
||||||
|
segment: Segment,
|
||||||
|
level: number
|
||||||
|
): Promise<I> => {
|
||||||
|
// console.debug(segment, level);
|
||||||
|
if (segment === rootSegment || level <= recursiveConfig.level - 1) {
|
||||||
|
const canvas = this.createCanvas(segment.w, segment.h);
|
||||||
|
const modeKey = choice(recursiveConfig.modes);
|
||||||
|
// console.debug(modeKey);
|
||||||
|
const mode = this.modes[modeKey];
|
||||||
|
const ctx = canvas.getContext("2d") as C;
|
||||||
|
const segments = mode.getSegments(ctx);
|
||||||
|
// console.debug(segments);
|
||||||
|
const bitmaps = await Promise.all(
|
||||||
|
segments.map((segment: Segment) => processSegment(segment, level + 1))
|
||||||
|
);
|
||||||
|
mode.place(ctx, bitmaps, segments);
|
||||||
|
return await this.canvasToImage(canvas);
|
||||||
|
} else {
|
||||||
|
if (recursiveConfig.repeat) {
|
||||||
|
return choice(localImages);
|
||||||
|
} else {
|
||||||
|
if (localImages.length > 0) {
|
||||||
|
return localImages.pop() as I;
|
||||||
|
} else {
|
||||||
|
throw "RAN OUT OF IMAGES";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await processSegment(rootSegment, 0);
|
||||||
|
this.drawImage(
|
||||||
|
context,
|
||||||
|
result,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
result.width,
|
||||||
|
result.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
context.canvas.width,
|
||||||
|
context.canvas.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanDraw(
|
||||||
|
ctx: C,
|
||||||
|
image: I,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number
|
||||||
|
) {
|
||||||
|
const scaleRatio = Math.max(w / image.width, h / image.height);
|
||||||
|
this.drawImage(
|
||||||
|
ctx,
|
||||||
|
image,
|
||||||
|
image.width / 2 - w / scaleRatio / 2,
|
||||||
|
image.height / 2 - h / scaleRatio / 2,
|
||||||
|
w / scaleRatio,
|
||||||
|
h / scaleRatio,
|
||||||
|
x - w / 2,
|
||||||
|
y - h / 2,
|
||||||
|
w,
|
||||||
|
h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
|
||||||
|
segments.forEach((segment, idx) => {
|
||||||
|
this.cleanDraw(
|
||||||
|
ctx,
|
||||||
|
images[idx],
|
||||||
|
segment.x,
|
||||||
|
segment.y,
|
||||||
|
segment.w,
|
||||||
|
segment.h
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGridSegments(ctx: C, config?: CollageConfig) {
|
||||||
|
return [0, 1, 2, 3].map((idx) => {
|
||||||
|
let x!: number, y!: number;
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
x = ctx.canvas.width * 0.25;
|
||||||
|
y = ctx.canvas.height * 0.25;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
x = ctx.canvas.width * 0.75;
|
||||||
|
y = ctx.canvas.height * 0.25;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
x = ctx.canvas.width * 0.25;
|
||||||
|
y = ctx.canvas.height * 0.75;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
x = ctx.canvas.width * 0.75;
|
||||||
|
y = ctx.canvas.height * 0.75;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w: ctx.canvas.width / 2,
|
||||||
|
h: ctx.canvas.height / 2,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract createCanvas(w: number, h: number): CS;
|
||||||
|
|
||||||
|
abstract canvasToImage(canvas: CS): PromiseLike<I>;
|
||||||
|
|
||||||
|
abstract drawImage(
|
||||||
|
ctx: C,
|
||||||
|
image: I,
|
||||||
|
sx: number,
|
||||||
|
sy: number,
|
||||||
|
sw: number,
|
||||||
|
sh: number,
|
||||||
|
dx: number,
|
||||||
|
dy: number,
|
||||||
|
dw: number,
|
||||||
|
dh: number
|
||||||
|
): void;
|
||||||
|
}
|
35
src/common/types.ts
Normal file
35
src/common/types.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export interface CollageMode<C extends CollageContext, I extends CollageImage> {
|
||||||
|
name: string;
|
||||||
|
minImages: number;
|
||||||
|
getSegments: (ctx: C, config?: CollageConfig, images?: I[]) => Segment[];
|
||||||
|
place: (ctx: C, images: I[], segments: Segment[]) => void;
|
||||||
|
forceConfig?: CollageConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollageConfig {
|
||||||
|
numImages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Segment {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface CollageContext {
|
||||||
|
globalCompositeOperation: string;
|
||||||
|
canvas: CollageCanvas
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollageCanvas {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
getContext: (x: '2d') => CollageContext | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollageImage {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
|
@ -18,6 +18,10 @@ export function randint(n: number) {
|
||||||
return Math.floor(Math.random() * n);
|
return Math.floor(Math.random() * n);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function choice<T>(arr: T[]): T {
|
export function choice<T>(arr: readonly T[]): T {
|
||||||
return arr[randint(arr.length)];
|
return arr[randint(arr.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function range(n: number): number[] {
|
||||||
|
return Array.from({length: n}, (x, i) => i);
|
||||||
|
}
|
|
@ -1,124 +1,281 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="collage">
|
<div class="collage">
|
||||||
<div class="canvas">
|
<div class="canvas">
|
||||||
<canvas id="canvas" ref="canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas>
|
<canvas
|
||||||
<div class="canvas-size">
|
id="canvas"
|
||||||
<label>
|
ref="canvas"
|
||||||
Width:
|
:width="canvasSize.width"
|
||||||
<input type="number" step="16" min="128" v-model="canvasSize.width">
|
:height="canvasSize.height"
|
||||||
</label>
|
></canvas>
|
||||||
<label>
|
<div class="canvas-size">
|
||||||
Height:
|
<label>
|
||||||
<input type="number" step="16" min="128" v-model="canvasSize.height">
|
Width:
|
||||||
</label>
|
<input type="number" step="16" min="128" v-model="canvasSize.width" />
|
||||||
</div>
|
</label>
|
||||||
|
<label>
|
||||||
|
Height:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="16"
|
||||||
|
min="128"
|
||||||
|
v-model="canvasSize.height"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
<label v-for="(mode, idx) in modes"
|
<ul class="modes-list">
|
||||||
:class="{disabled: images.length < mode.minImages,
|
<li
|
||||||
selected: idx === currentModeType,
|
v-for="(mode, idx) in modes.modes"
|
||||||
lastActive: idx === lastActiveModeType}">
|
:class="[
|
||||||
{{mode.name}}
|
'mode',
|
||||||
<input type="radio" :value="idx" v-model="currentModeType">
|
{
|
||||||
</label>
|
disabled: images.length < mode.minImages,
|
||||||
</div>
|
excluded: excludedModes.includes(idx),
|
||||||
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button>
|
selected: idx === currentModeType,
|
||||||
<hr>
|
lastActive: lastActiveModeTypes.includes(idx),
|
||||||
<div class="config">
|
},
|
||||||
<label class="config-numimages">
|
]"
|
||||||
#N of images:
|
:key="idx"
|
||||||
<input type="number" :min="currentMode.minImages" :max="images.length"
|
>
|
||||||
placeholder="RND"
|
<label class="handle"
|
||||||
:disabled="Object.keys(forceConfig).includes('numImages')"
|
>{{ mode.name
|
||||||
v-model="forceConfig.numImages || collageConfig.numImages">
|
}}<input type="radio" :value="idx" v-model="currentModeType"
|
||||||
</label>
|
/></label>
|
||||||
</div>
|
<span
|
||||||
|
class="mode-plus"
|
||||||
|
v-if="
|
||||||
|
['recursive', 'shuffle'].includes(currentModeType) &&
|
||||||
|
images.length >= minImages
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="separator"></span>
|
||||||
|
<label
|
||||||
|
>{{ excludedModes.includes(idx) ? "o" : "x"
|
||||||
|
}}<input type="checkbox" :value="idx" v-model="excludedModes"
|
||||||
|
/></label>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<label
|
||||||
|
:class="{
|
||||||
|
disabled: images.length < minImages,
|
||||||
|
selected: 'shuffle' === currentModeType,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
!SHUFFLE ALL!
|
||||||
|
<input type="radio" value="shuffle" v-model="currentModeType" />
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
:class="{
|
||||||
|
disabled: images.length < minImages,
|
||||||
|
selected: 'recursive' === currentModeType,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
#RECURSIVE#
|
||||||
|
<input type="radio" value="recursive" v-model="currentModeType" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button :disabled="images.length < minImages" @click="renderCollage">
|
||||||
|
REPAINT
|
||||||
|
</button>
|
||||||
|
<hr />
|
||||||
|
<div class="config">
|
||||||
|
<template v-if="currentModeType !== 'recursive'">
|
||||||
|
<label class="config-numimages">
|
||||||
|
#N of images:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:min="minImages"
|
||||||
|
:max="images.length"
|
||||||
|
placeholder="RND"
|
||||||
|
:disabled="Object.keys(forceConfig).includes('numImages')"
|
||||||
|
:value="forceConfig.numImages || collageConfig.numImages"
|
||||||
|
@input="(n) => (collageConfig.numImages = n)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<label>
|
||||||
|
Recursion levels:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
v-model="recursiveConfig.level"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="recursiveConfig.repeat" />
|
||||||
|
Repeat images?
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import collageModes, {CollageModeType} from "../collages";
|
import BrowserCollageModes from "../collages";
|
||||||
import {CollageConfig, CollageMode} from "@/types";
|
import { CollageModeType } from "../common/collages";
|
||||||
|
import { CollageConfig, CollageMode, Segment } from "../common/types";
|
||||||
|
import { choice, shuffle } from "../common/utils";
|
||||||
|
|
||||||
type DisplayCollageModeType = CollageModeType | & "shuffle";
|
type DisplayCollageModeType = CollageModeType | "shuffle" | "recursive";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class Collage extends Vue {
|
export default class Collage extends Vue {
|
||||||
@Prop({required: true}) private images!: ImageBitmap[];
|
@Prop({ required: true }) private images!: ImageBitmap[];
|
||||||
private context!: CanvasRenderingContext2D;
|
private context!: CanvasRenderingContext2D;
|
||||||
private canvasSize = {
|
private canvasSize = {
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 640
|
height: 640,
|
||||||
};
|
};
|
||||||
private collageConfig: CollageConfig = {
|
private collageConfig: CollageConfig = {
|
||||||
numImages: undefined
|
numImages: undefined,
|
||||||
};
|
};
|
||||||
private currentModeType: DisplayCollageModeType = "shuffle";
|
private recursiveConfig = {
|
||||||
private lastActiveModeType: DisplayCollageModeType | null = null;
|
level: 2,
|
||||||
private modes: { [key in DisplayCollageModeType]: CollageMode } = {
|
repeat: true,
|
||||||
...collageModes,
|
};
|
||||||
"shuffle": {
|
private currentModeType: DisplayCollageModeType = "shuffle";
|
||||||
name: "Shuffle all!",
|
private lastActiveModeTypes: CollageModeType[] = [];
|
||||||
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)),
|
private excludedModes: CollageModeType[] = [];
|
||||||
place: (ctx, images, config) => {
|
private modes = new BrowserCollageModes();
|
||||||
const permissibleModeKeys = Object.keys(collageModes)
|
|
||||||
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[];
|
private get minImages() {
|
||||||
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)];
|
if (
|
||||||
const randomMode = collageModes[randomModeType];
|
this.currentModeType === "shuffle" ||
|
||||||
this.setLastActiveModeType(randomModeType);
|
this.currentModeType === "recursive"
|
||||||
randomMode.place(ctx, images, config);
|
) {
|
||||||
|
return Math.min(
|
||||||
|
...Object.values(this.modes.modes).map((mode) => mode.minImages)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.modes.modes[this.currentModeType].minImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get lastMode() {
|
||||||
|
if (this.lastActiveModeTypes.length === 1) {
|
||||||
|
return this.modes.modes[this.lastActiveModeTypes[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get forceConfig() {
|
||||||
|
return this.lastMode ? this.lastMode.forceConfig || {} : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mounted() {
|
||||||
|
const canvas = this.$refs.canvas as HTMLCanvasElement;
|
||||||
|
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("images")
|
||||||
|
@Watch("currentModeType")
|
||||||
|
@Watch("collageConfig", { deep: true })
|
||||||
|
@Watch("recursiveConfig", { deep: true })
|
||||||
|
private renderCollage() {
|
||||||
|
if (this.images.length >= this.minImages) {
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
const permissibleModeKeys = (
|
||||||
|
Object.keys(this.modes.modes) as CollageModeType[]
|
||||||
|
).filter(
|
||||||
|
(k) =>
|
||||||
|
!this.excludedModes.includes(k) &&
|
||||||
|
this.modes.modes[k].minImages <= this.images.length
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.currentModeType !== "recursive") {
|
||||||
|
let mode: CollageMode<any, any>;
|
||||||
|
if (this.currentModeType === "shuffle") {
|
||||||
|
const randomModeType = choice(permissibleModeKeys);
|
||||||
|
this.lastActiveModeTypes = [randomModeType];
|
||||||
|
mode = this.modes.modes[randomModeType];
|
||||||
|
} else {
|
||||||
|
this.lastActiveModeTypes = [this.currentModeType];
|
||||||
|
mode = this.modes.modes[this.currentModeType];
|
||||||
|
}
|
||||||
|
const shuffledImages = shuffle(this.images);
|
||||||
|
const segments = mode.getSegments(
|
||||||
|
this.context,
|
||||||
|
this.collageConfig,
|
||||||
|
shuffledImages
|
||||||
|
);
|
||||||
|
mode.place(this.context, shuffledImages, segments);
|
||||||
|
} else {
|
||||||
|
this.lastActiveModeTypes = [];
|
||||||
|
const shuffledImages = shuffle(this.images);
|
||||||
|
const rootSegment: Segment = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: this.context.canvas.width,
|
||||||
|
h: this.context.canvas.height,
|
||||||
|
};
|
||||||
|
const processSegment = async (
|
||||||
|
segment: Segment,
|
||||||
|
level: number
|
||||||
|
): Promise<ImageBitmap> => {
|
||||||
|
console.debug(segment, level);
|
||||||
|
if (
|
||||||
|
segment === rootSegment ||
|
||||||
|
level <= this.recursiveConfig.level - 1
|
||||||
|
) {
|
||||||
|
let canvas = document.createElement("canvas");
|
||||||
|
canvas.width = segment.w;
|
||||||
|
canvas.height = segment.h;
|
||||||
|
let modeKey = choice(permissibleModeKeys);
|
||||||
|
console.debug(modeKey);
|
||||||
|
this.lastActiveModeTypes.push(modeKey);
|
||||||
|
let mode = this.modes.modes[modeKey];
|
||||||
|
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
let segments = mode.getSegments(ctx);
|
||||||
|
console.debug(segments);
|
||||||
|
let bitmaps = await Promise.all(
|
||||||
|
segments.map((segment) => processSegment(segment, level + 1))
|
||||||
|
);
|
||||||
|
mode.place(ctx, bitmaps, segments);
|
||||||
|
return await createImageBitmap(canvas);
|
||||||
|
} else {
|
||||||
|
if (this.recursiveConfig.repeat) {
|
||||||
|
return choice(shuffledImages);
|
||||||
|
} else {
|
||||||
|
if (shuffledImages.length > 0) {
|
||||||
|
return shuffledImages.pop() as ImageBitmap;
|
||||||
|
} else {
|
||||||
|
throw "RAN OUT OF IMAGES";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
processSegment(rootSegment, 0)
|
||||||
// wtf vue?
|
.then((finalCollage) => {
|
||||||
private setLastActiveModeType(lastActiveModeType: any) {
|
console.debug(finalCollage);
|
||||||
this.lastActiveModeType = lastActiveModeType;
|
this.context.drawImage(finalCollage, 0, 0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private get currentMode() {
|
@Watch("canvasSize", { deep: true })
|
||||||
return this.modes[this.currentModeType];
|
private onCanvasSizeChange() {
|
||||||
}
|
this.$nextTick(() => {
|
||||||
|
this.renderCollage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private get lastMode() {
|
private reset() {
|
||||||
return this.lastActiveModeType ? this.modes[this.lastActiveModeType] : undefined;
|
this.context.globalCompositeOperation = "source-over";
|
||||||
}
|
const canvas = this.$refs.canvas as HTMLCanvasElement;
|
||||||
|
this.context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
private get forceConfig() {
|
}
|
||||||
return this.lastMode ? this.lastMode.forceConfig || {} : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private mounted() {
|
|
||||||
const canvas = (this.$refs.canvas as HTMLCanvasElement);
|
|
||||||
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("images")
|
|
||||||
@Watch("currentMode")
|
|
||||||
@Watch("collageConfig", {deep: true})
|
|
||||||
private renderCollage() {
|
|
||||||
if (this.images.length >= this.currentMode.minImages) {
|
|
||||||
this.lastActiveModeType = this.currentModeType;
|
|
||||||
this.reset();
|
|
||||||
this.currentMode.place(this.context, this.images, this.collageConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("canvasSize", {deep: true})
|
|
||||||
private onCanvasSizeChange() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.renderCollage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private reset() {
|
|
||||||
this.context.globalCompositeOperation = "source-over";
|
|
||||||
const canvas = (this.$refs.canvas as HTMLCanvasElement);
|
|
||||||
this.context.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -126,72 +283,115 @@ export default class Collage extends Vue {
|
||||||
<!--suppress CssUnusedSymbol -->
|
<!--suppress CssUnusedSymbol -->
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.collage {
|
.collage {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas {
|
#canvas {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-size {
|
.canvas-size {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls button {
|
.controls button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modes {
|
.modes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modes-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
border: 1px solid gray;
|
||||||
|
width: 0;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode .mode-plus,
|
||||||
|
.mode .mode-plus label {
|
||||||
|
color: gray;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-plus {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls .modes hr {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
color: lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls label {
|
.controls label {
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: .25rem;
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modes input {
|
.modes input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls button, .controls hr, .controls .config {
|
.controls button,
|
||||||
margin-top: 1rem;
|
.controls hr,
|
||||||
|
.controls .config {
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .config {
|
.controls .config {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config label {
|
.config label {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-numimages input {
|
.config-numimages input {
|
||||||
width: 4em;
|
width: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
color: gray;
|
color: gray;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excluded .handle {
|
||||||
|
color: gray;
|
||||||
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lastActive {
|
.lastActive .handle {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
10
src/types.d.ts
vendored
10
src/types.d.ts
vendored
|
@ -1,10 +0,0 @@
|
||||||
export interface CollageMode {
|
|
||||||
name: string;
|
|
||||||
minImages: number;
|
|
||||||
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], config: CollageConfig) => void;
|
|
||||||
forceConfig?: CollageConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollageConfig {
|
|
||||||
numImages?: number;
|
|
||||||
}
|
|
22
tgbot/Dockerfile
Normal file
22
tgbot/Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
FROM python:3-slim
|
||||||
|
|
||||||
|
ENV PYTHONFAULTHANDLER=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONHASHSEED=random \
|
||||||
|
PIP_NO_CACHE_DIR=off \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||||
|
PIP_DEFAULT_TIMEOUT=100
|
||||||
|
|
||||||
|
RUN pip install poetry
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY tgbot/poetry.lock tgbot/pyproject.toml /app/
|
||||||
|
|
||||||
|
RUN poetry config virtualenvs.create false \
|
||||||
|
&& poetry install --no-dev --no-interaction --no-ansi
|
||||||
|
|
||||||
|
COPY /tgbot/ /app/
|
||||||
|
|
||||||
|
COPY kollagen /usr/bin/kollagen
|
||||||
|
|
||||||
|
CMD python kollagen-bot/main.py
|
0
tgbot/README.rst
Normal file
0
tgbot/README.rst
Normal file
BIN
tgbot/kollagen
Executable file
BIN
tgbot/kollagen
Executable file
Binary file not shown.
1
tgbot/kollagen-bot/__init__.py
Normal file
1
tgbot/kollagen-bot/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__version__ = '0.1.0'
|
270
tgbot/kollagen-bot/main.py
Normal file
270
tgbot/kollagen-bot/main.py
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import (
|
||||||
|
Updater,
|
||||||
|
CommandHandler,
|
||||||
|
DictPersistence,
|
||||||
|
CallbackContext,
|
||||||
|
)
|
||||||
|
from telegram.parsemode import ParseMode
|
||||||
|
from parser import SafeArgumentParser, safe_str
|
||||||
|
|
||||||
|
|
||||||
|
class KollagenBot:
|
||||||
|
def __init__(
|
||||||
|
self, tg_token: str, kollagen_path: str, base_dir: Optional[str]
|
||||||
|
) -> None:
|
||||||
|
self.logger = logging.getLogger("kollagen")
|
||||||
|
self.kollagen_path = kollagen_path
|
||||||
|
self.base_dir = os.path.abspath(base_dir) if base_dir else None
|
||||||
|
|
||||||
|
self._init_parser()
|
||||||
|
|
||||||
|
self.updater = Updater(
|
||||||
|
tg_token, persistence=DictPersistence(user_data_json="{}")
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatcher = self.updater.dispatcher
|
||||||
|
|
||||||
|
dispatcher.add_handler(CommandHandler("start", self.tg_start))
|
||||||
|
dispatcher.add_handler(CommandHandler("help", self.tg_help))
|
||||||
|
dispatcher.add_handler(CommandHandler("generate", self.tg_generate))
|
||||||
|
dispatcher.add_handler(CommandHandler("g", self.tg_generate))
|
||||||
|
dispatcher.add_handler(CommandHandler("regenerate", self.tg_regenerate))
|
||||||
|
dispatcher.add_handler(CommandHandler("r", self.tg_regenerate))
|
||||||
|
dispatcher.add_error_handler(self.tg_error)
|
||||||
|
|
||||||
|
def _init_parser(self):
|
||||||
|
parser = SafeArgumentParser(prog="/generate", add_help=False)
|
||||||
|
parser.add_argument(
|
||||||
|
"directories",
|
||||||
|
metavar="path",
|
||||||
|
type=safe_str,
|
||||||
|
nargs="*",
|
||||||
|
default=[self.base_dir] if self.base_dir else [],
|
||||||
|
help="Directories or files to process. By default, the entire base directory is processed.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-m",
|
||||||
|
dest="mode",
|
||||||
|
metavar="mode",
|
||||||
|
type=safe_str,
|
||||||
|
nargs="?",
|
||||||
|
const=True,
|
||||||
|
help=f"Collage modes to use. By default, one is chosen at random. Multiple modes can be specified, separated by commas. When no value is specified, all modes are listed.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
dest="num_images",
|
||||||
|
metavar="N",
|
||||||
|
type=int,
|
||||||
|
help=f"How many images to use in a single collage. Random (or collage-dependant) by default.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-w",
|
||||||
|
dest="width",
|
||||||
|
type=int,
|
||||||
|
default=640,
|
||||||
|
help=f"Width of resulting output (in px). 640px by default.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-h",
|
||||||
|
dest="height",
|
||||||
|
type=int,
|
||||||
|
default=640,
|
||||||
|
help=f"Height of resulting output (in px). 640px by default.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--rm",
|
||||||
|
dest="recursive_modes",
|
||||||
|
type=safe_str,
|
||||||
|
help=f"Collage modes (comma-separated) to use in a recursive collage. All by default.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--rl",
|
||||||
|
dest="recursive_level",
|
||||||
|
type=int,
|
||||||
|
default=2,
|
||||||
|
help=f"Level/depth of recursive collage. 2 by default.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--rr",
|
||||||
|
dest="recursive_repeat",
|
||||||
|
action="store_true",
|
||||||
|
help=f"Allow repeating images in (different levels of) recursive collages. False by default.",
|
||||||
|
)
|
||||||
|
self.parser = parser
|
||||||
|
|
||||||
|
def _get_modes(self):
|
||||||
|
modes = subprocess.run(
|
||||||
|
[self.kollagen_path, "-m"], check=True, capture_output=True
|
||||||
|
)
|
||||||
|
return modes.stdout.decode("utf-8").strip().split(", ")
|
||||||
|
|
||||||
|
def tg_start(self, update: Update, context: CallbackContext):
|
||||||
|
update.message.reply_text("Hi! I make random collages. Check out https://gitlab.com/tmladek/kollagen and /help. Here's one to get you started:")
|
||||||
|
self._process([], update)
|
||||||
|
|
||||||
|
def tg_generate(self, update: Update, context: CallbackContext):
|
||||||
|
cmd_line = update.message.text.split(" ")[1:]
|
||||||
|
success = self._process(cmd_line, update)
|
||||||
|
if success and context.user_data is not None:
|
||||||
|
context.user_data["last_cmd_line"] = cmd_line
|
||||||
|
|
||||||
|
def tg_regenerate(self, update: Update, context: CallbackContext):
|
||||||
|
if context.user_data and context.user_data.get("last_cmd_line"):
|
||||||
|
self._process(context.user_data["last_cmd_line"], update)
|
||||||
|
else:
|
||||||
|
update.message.reply_text("No previous command to regenerate!")
|
||||||
|
|
||||||
|
def _process(self, cmd_line: List[str], update: Update):
|
||||||
|
self.logger.info(
|
||||||
|
f"Generating from {update.effective_user}, with cmd_line: `{cmd_line}`"
|
||||||
|
)
|
||||||
|
args = self.parser.parse_args(cmd_line)
|
||||||
|
|
||||||
|
if args.mode is True:
|
||||||
|
update.message.reply_text(
|
||||||
|
f"Available modes: {', '.join(self._get_modes())}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
directories = []
|
||||||
|
for dir in args.directories:
|
||||||
|
for possible in [
|
||||||
|
os.path.join(self.base_dir or "./", dir),
|
||||||
|
os.path.join(self.base_dir or "./", dir.upper()),
|
||||||
|
]:
|
||||||
|
if os.path.exists(possible):
|
||||||
|
directories.append(possible)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(f'"{dir}" does not exist.')
|
||||||
|
|
||||||
|
mode = ["-m", args.mode] if args.mode else []
|
||||||
|
num_images = ["-n", str(args.num_images)] if args.num_images else []
|
||||||
|
recursive_level = (
|
||||||
|
["--rl", str(args.recursive_level)] if args.recursive_level else []
|
||||||
|
)
|
||||||
|
recursive_repeat = ["--rr"] if args.recursive_repeat else []
|
||||||
|
recursive_modes = (
|
||||||
|
["--rm", str(args.recursive_modes)] if args.recursive_modes else []
|
||||||
|
)
|
||||||
|
|
||||||
|
with NamedTemporaryFile(suffix=".png") as ntf:
|
||||||
|
shell_cmd_line = [
|
||||||
|
self.kollagen_path,
|
||||||
|
*directories,
|
||||||
|
"-w",
|
||||||
|
str(args.width),
|
||||||
|
"-h",
|
||||||
|
str(args.height),
|
||||||
|
*mode,
|
||||||
|
*num_images,
|
||||||
|
*recursive_level,
|
||||||
|
*recursive_repeat,
|
||||||
|
*recursive_modes,
|
||||||
|
"-o",
|
||||||
|
ntf.name,
|
||||||
|
]
|
||||||
|
|
||||||
|
self.logger.debug(f"Running: " + str(shell_cmd_line))
|
||||||
|
result = subprocess.run(
|
||||||
|
shell_cmd_line,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=60,
|
||||||
|
env={
|
||||||
|
'NO_COLOR': "1"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ntf.seek(0)
|
||||||
|
|
||||||
|
used_line = next(
|
||||||
|
(
|
||||||
|
line
|
||||||
|
for line in result.stdout.decode("utf-8").splitlines()
|
||||||
|
if line.startswith("Used: ")
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
).replace(f"{self.base_dir}/" if self.base_dir else "", "")
|
||||||
|
|
||||||
|
caption = ""
|
||||||
|
caption += (
|
||||||
|
f"`{' '.join(['/generate', *cmd_line])}`\n" if len(cmd_line) else ""
|
||||||
|
)
|
||||||
|
caption += used_line.replace("_", "\\_")
|
||||||
|
caption = caption[:200]
|
||||||
|
|
||||||
|
update.message.reply_photo(
|
||||||
|
ntf,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def tg_help(self, update: Update, context: CallbackContext):
|
||||||
|
update.message.reply_text(
|
||||||
|
f"```{self.parser.format_help()}```", parse_mode=ParseMode.MARKDOWN
|
||||||
|
)
|
||||||
|
|
||||||
|
def tg_error(self, update: object, context: CallbackContext) -> None:
|
||||||
|
self.logger.error(
|
||||||
|
msg="Exception while handling an update:", exc_info=context.error
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(update, Update):
|
||||||
|
if isinstance(context.error, subprocess.CalledProcessError):
|
||||||
|
error_display = context.error.stderr.decode('utf-8')
|
||||||
|
else:
|
||||||
|
error_display = str(context.error)
|
||||||
|
error_display = error_display[:2500]
|
||||||
|
update.message.reply_text(
|
||||||
|
f"Something is fucked!\n{error_display}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_idle(self):
|
||||||
|
self.updater.start_polling()
|
||||||
|
self.updater.idle()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
level=getattr(logging, os.getenv("LOG_LEVEL", "info").upper()),
|
||||||
|
)
|
||||||
|
|
||||||
|
tg_token = os.getenv("TG_TOKEN")
|
||||||
|
if not tg_token:
|
||||||
|
logging.error("TG_TOKEN is required.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if env_kollagen_path := os.getenv("KOLLAGEN_PATH"):
|
||||||
|
if os.path.exists(env_kollagen_path):
|
||||||
|
kollagen_path = env_kollagen_path
|
||||||
|
else:
|
||||||
|
logging.error(f"kollagen not found! {env_kollagen_path} does not exist.")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
which = subprocess.run(["which", "kollagen"], capture_output=True)
|
||||||
|
try:
|
||||||
|
which.check_returncode()
|
||||||
|
kollagen_path = which.stdout.decode("utf-8").strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logging.error(
|
||||||
|
"kollagen not found! KOLLAGEN_PATH not specified and `kollagen` isn't in $PATH."
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
bot = KollagenBot(tg_token, kollagen_path, os.getenv("BASE_DIR"))
|
||||||
|
bot.start_idle()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
16
tgbot/kollagen-bot/parser.py
Normal file
16
tgbot/kollagen-bot/parser.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class ArgumentParserError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SafeArgumentParser(argparse.ArgumentParser):
|
||||||
|
def error(self, message):
|
||||||
|
raise ArgumentParserError(message)
|
||||||
|
|
||||||
|
def safe_str(val: str):
|
||||||
|
if re.findall(r'[^\w,]', val):
|
||||||
|
raise RuntimeError("No special characters in arguments allowed!")
|
||||||
|
return val
|
393
tgbot/poetry.lock
generated
Normal file
393
tgbot/poetry.lock
generated
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
[[package]]
|
||||||
|
name = "apscheduler"
|
||||||
|
version = "3.6.3"
|
||||||
|
description = "In-process task scheduler with Cron-like capabilities"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytz = "*"
|
||||||
|
six = ">=1.4.0"
|
||||||
|
tzlocal = ">=1.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
asyncio = ["trollius"]
|
||||||
|
doc = ["sphinx", "sphinx-rtd-theme"]
|
||||||
|
gevent = ["gevent"]
|
||||||
|
mongodb = ["pymongo (>=2.8)"]
|
||||||
|
redis = ["redis (>=3.0)"]
|
||||||
|
rethinkdb = ["rethinkdb (>=2.4.0)"]
|
||||||
|
sqlalchemy = ["sqlalchemy (>=0.8)"]
|
||||||
|
testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"]
|
||||||
|
tornado = ["tornado (>=4.3)"]
|
||||||
|
twisted = ["twisted"]
|
||||||
|
zookeeper = ["kazoo"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backports.zoneinfo"
|
||||||
|
version = "0.2.1"
|
||||||
|
description = "Backport of the standard library zoneinfo module"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tzdata = ["tzdata"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "21.9b0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.2"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=7.1.2"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
pathspec = ">=0.9.0,<1"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
regex = ">=2020.1.8"
|
||||||
|
tomli = ">=0.2.6,<2.0.0"
|
||||||
|
typing-extensions = [
|
||||||
|
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
|
||||||
|
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
python2 = ["typed-ast (>=1.4.2)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "4.2.2"
|
||||||
|
description = "Extensible memoizing collections and decorators"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "~=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2021.5.30"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.0.1"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "0.4.3"
|
||||||
|
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.9.0"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "2.3.0"
|
||||||
|
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-telegram-bot"
|
||||||
|
version = "13.7"
|
||||||
|
description = "We have made you a wrapper you can't refuse"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
APScheduler = "3.6.3"
|
||||||
|
cachetools = "4.2.2"
|
||||||
|
certifi = "*"
|
||||||
|
pytz = ">=2018.6"
|
||||||
|
tornado = ">=6.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
json = ["ujson"]
|
||||||
|
passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"]
|
||||||
|
socks = ["pysocks"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2021.1"
|
||||||
|
description = "World timezone definitions, modern and historical"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "2021.8.28"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "1.2.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tornado"
|
||||||
|
version = "6.1"
|
||||||
|
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">= 3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "3.10.0.2"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2021.1"
|
||||||
|
description = "Provider of IANA time zone data"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "3.0"
|
||||||
|
description = "tzinfo object for the local timezone"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
|
||||||
|
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = "^3.8"
|
||||||
|
content-hash = "a1390250d2dc7c6ca705c53cebcec951a6dd79717bb0f902b0177b307fac8bc3"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
apscheduler = [
|
||||||
|
{file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"},
|
||||||
|
{file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"},
|
||||||
|
]
|
||||||
|
"backports.zoneinfo" = [
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
|
||||||
|
{file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
|
||||||
|
]
|
||||||
|
black = [
|
||||||
|
{file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
|
||||||
|
{file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
|
||||||
|
]
|
||||||
|
cachetools = [
|
||||||
|
{file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"},
|
||||||
|
{file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"},
|
||||||
|
]
|
||||||
|
certifi = [
|
||||||
|
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
|
||||||
|
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
|
||||||
|
]
|
||||||
|
click = [
|
||||||
|
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
|
||||||
|
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
|
||||||
|
]
|
||||||
|
colorama = [
|
||||||
|
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||||
|
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||||
|
]
|
||||||
|
mypy-extensions = [
|
||||||
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
|
]
|
||||||
|
pathspec = [
|
||||||
|
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||||
|
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||||
|
]
|
||||||
|
platformdirs = [
|
||||||
|
{file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
|
||||||
|
{file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
|
||||||
|
]
|
||||||
|
python-telegram-bot = [
|
||||||
|
{file = "python-telegram-bot-13.7.tar.gz", hash = "sha256:24df75459e335b96baffa6991679f844bd426978af5a69ca419a0ac43a40602c"},
|
||||||
|
{file = "python_telegram_bot-13.7-py3-none-any.whl", hash = "sha256:3bf210862744068aa789d5110f8e3a00d98912ce50863384836440a18abf76b5"},
|
||||||
|
]
|
||||||
|
pytz = [
|
||||||
|
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
|
||||||
|
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
|
||||||
|
]
|
||||||
|
regex = [
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
|
||||||
|
{file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
|
||||||
|
{file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
|
||||||
|
{file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
|
||||||
|
{file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
|
||||||
|
{file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
|
||||||
|
{file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
|
||||||
|
]
|
||||||
|
six = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
]
|
||||||
|
tomli = [
|
||||||
|
{file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
|
||||||
|
{file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
|
||||||
|
]
|
||||||
|
tornado = [
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
|
||||||
|
{file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
|
||||||
|
{file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
|
||||||
|
{file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
|
||||||
|
{file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
|
||||||
|
{file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
|
||||||
|
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||||
|
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
|
||||||
|
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
|
||||||
|
]
|
||||||
|
tzdata = [
|
||||||
|
{file = "tzdata-2021.1-py2.py3-none-any.whl", hash = "sha256:9ad21eada54c97001e3e9858a674b3ee6bebe4a4fb2b58465930f2af0ba6c85d"},
|
||||||
|
{file = "tzdata-2021.1.tar.gz", hash = "sha256:e19c7351f887522a1ac739d21041e592ddde6dd1b764fdefa8f7b2b3551d3d38"},
|
||||||
|
]
|
||||||
|
tzlocal = [
|
||||||
|
{file = "tzlocal-3.0-py3-none-any.whl", hash = "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559"},
|
||||||
|
{file = "tzlocal-3.0.tar.gz", hash = "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12"},
|
||||||
|
]
|
16
tgbot/pyproject.toml
Normal file
16
tgbot/pyproject.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "kollagen-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Tomáš Mládek <t@mldk.cz>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
python-telegram-bot = "^13.7"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
black = {version = "^21.9b0", allow-prereleases = true}
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
Loading…
Reference in a new issue