Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add jsdoc comments and update readme #183

Merged
merged 7 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 160 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ Easily generate images on-the-fly with node.js using wide range of templates.
- image templates _(wip)_
- image filters _(wip)_
- complex layouts _(wip)_
- templates api _(wip)_
- builder api _(wip)_

## Example

### Image Generation

#### Using built-in templates (New "Legacy api")

<!-- prettier-ignore -->
```ts
import { canvacord } from 'canvacord';
Expand All @@ -29,6 +33,10 @@ import fs from 'node:fs';
const triggered = await canvacord.triggered(image);
triggered.pipe(fs.createWriteStream('triggered.gif'));

// image generation
const beautiful = await canvacord.beautiful(img);
const facepalm = await canvacord.facepalm(img);

// filters
const filtered = await canvacord
.filters(512, 512)
Expand All @@ -52,6 +60,157 @@ const filtered = await canvacord(image, 512, 512)
fs.writeFileSync('filtered.png', filtered);
```

## XP Card Preview
## XP Card

```ts
import { Font, RankCardBuilder } from 'canvacord';
import { writeFile } from 'fs/promises';

// load default font
Font.loadDefault();

const card = new RankCardBuilder()
.setUsername('Lost Ctrl')
.setDisplayName('thearchaeopteryx')
.setAvatar('...')
.setCurrentXP(3800)
.setRequiredXP(2500)
.setLevel(54)
.setRank(32)
.setStatus('online');

const image = await card.build({
format: 'png'
});

await writeFileSync('./card.png', data);
```

![xp-card](https://raw.githubusercontent.com/neplextech/canvacord/main/test/jsx/test2.svg)

## Creating images using custom template

```ts
import { createTemplate, ImageFactory, TemplateImage, createImageGenerator } from 'canvacord';

const AffectedMeme = createTemplate((image: ImageSource) => {
return {
steps: [
{
image: [
{
source: new TemplateImage(ImageFactory.AFFECT),
x: 0,
y: 0
}
]
},
{
image: [
{
source: new TemplateImage(image),
x: 180,
y: 383,
width: 200,
height: 157
}
]
}
]
};
});

// get target photo to use on "affected" meme image
const photo = await getPhotoForMemeSomehow();
const generator = createImageGenerator(AffectedMeme(photo));

// render out the image
await generator.render();

// get the resulting image in png format
const affectedMeme = await generator.encode('png');
```

#### Result

![output](https://raw.githubusercontent.com/neplextech/canvacord/main/test/canvas/affected.png)

## Creating images using custom builder

This is an advanced method of creating images. Canvacord builder api allows you to create your own image generator using JSX elements and a **subset of tailwind class names**. This is also possible without JSX, you can find an example [here](https://github.com/neplextech/canvacord/blob/7651c1aa51a844c2591cbe68a6e21eb9d1d6287a/benchmark/jsx-renderer.mjs).

> **Note**
> It does not support many css features such as grid layout. You can use flexbox instead.

If you want to use JSX with typescript, you need to add the following options to your `tsconfig.json`:

```jsonc
{
"compilerOptions": {
// other options
"jsx": "react",
"jsxFactory": "JSX.createElement",
"jsxFragmentFactory": "JSX.Fragment"
}
// other options
}
```

You can also use pragma comments to define JSX factory and fragment factory:

```js
/** @jsx JSX.createElement */
/** @jsxFrag JSX.Fragment */
```

```tsx
// JSX import is required if you want to use JSX syntax
// Builder is a base class to create your own builders
// Font is a utility class to load fonts
import { JSX, Builder, Font } from 'canvacord';
import { writeFile } from 'fs/promises';

// declare props types
interface Props {
text: string;
}

class Design extends Builder<Props> {
constructor() {
// set width and height
super(500, 500);
// initialize props
this.bootstrap({ text: '' });
}

// define custom methods for your builder
setText(text: string) {
this.options.set('text', text);
return this;
}

// this is where you have to define how the resulting image should look like
async render() {
return (
<div className="flex items-center justify-center h-full w-full bg-teal-500">
<h1 className="text-white font-bold text-7xl">{this.options.get('text')}</h1>
</div>
);
}
}

// usage
// load font
Font.loadDefault();

// create design
const design = new Design().setText('Hello World');
const image = await design.build({ format: 'png' });

// do something with generated image
await writeFile('./test.png', image);
```

#### Result

![output](https://github.com/neplextech/canvacord/assets/46562212/c50d09d6-33c4-4b44-81c2-aed6783f503c)
3 changes: 3 additions & 0 deletions src/assets/AssetsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const FontFactory = new Map<string, Font>();
const BASE_URL = process.env.CANVACORD_ASSETS_BASE_URL || 'https://cdn.neplextech.com/canvacord';
const prepareURL = (path: string) => `${BASE_URL}/${path}`;

/**
* The image assets factory.
*/
export const ImageFactory = {
AFFECT: prepareURL('AFFECT.png'),
BATSLAP: prepareURL('BATSLAP.png'),
Expand Down
52 changes: 52 additions & 0 deletions src/assets/Font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,30 @@ import { Fonts } from './fonts/fonts';
const randomAlias = () => randomUUID() as string;

export class Font {
/**
* Creates and registers a new Font instance for both canvas and builder apis.
* @param data The font data
* @param [alias] The font alias. If not provided, a random UUID will be used.
* @example ```typescript
* const data = await readFile('path/to/font.ttf');
* const font = new Font(data, 'my-font');
* ```
*/
public constructor(public data: Buffer, public alias = randomAlias()) {
GlobalFonts.register(data, alias);
FontFactory.set(this.alias, this);
}

/**
* The alias for this font.
*/
public get name() {
return this.alias;
}

/**
* Returns the font data that includes information such as the font name, weight, data, and style.
*/
public getData(): FontData {
return {
data: this.data,
Expand All @@ -27,28 +42,65 @@ export class Font {
};
}

/**
* String representation of this font.
*/
public toString() {
return this.alias;
}

/**
* JSON representation of this font.
*/
public toJSON() {
return this.getData();
}

/**
* Creates a new Font instance from a file.
* @param path The path to the font file
* @param [alias] The font alias. If not provided, a random UUID will be used.
* @example ```typescript
* const font = await Font.fromFile('path/to/font.ttf', 'my-font');
* ```
*/
public static async fromFile(path: string, alias?: string) {
const buffer = await readFile(path);
return new Font(buffer, alias);
}

/**
* Creates a new Font instance from a file synchronously.
* @param path The path to the font file
* @param [alias] The font alias. If not provided, a random UUID will be used.
* @example ```typescript
* const font = Font.fromFileSync('path/to/font.ttf', 'my-font');
* ```
*/
public static fromFileSync(path: string, alias?: string) {
const buffer = readFileSync(path);
return new Font(buffer, alias);
}

/**
* Creates a new Font instance from a buffer.
* @param buffer The buffer containing the font data
* @param [alias] The font alias. If not provided, a random UUID will be used.
* @example ```typescript
* const buffer = await readFile('path/to/font.ttf');
* const font = Font.fromBuffer(buffer, 'my-font');
* ```
*/
public static fromBuffer(buffer: Buffer, alias?: string) {
return new Font(buffer, alias);
}

/**
* Loads the default font bundled with this package.
* @example ```typescript
* const font = Font.loadDefault();
* ```
*/
public static loadDefault() {
return this.fromBuffer(Fonts.Geist, 'geist');
}
Expand Down
21 changes: 21 additions & 0 deletions src/assets/TemplateFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,36 @@ import type { Image, SKRSContext2D } from '@napi-rs/canvas';

export class TemplateImage {
#resolved: Image | null = null;

/**
* Creates a new TemplateImage instance.
* @param source The image source
* @example ```typescript
* const image = new TemplateImage('https://example.com/image.png');
* ```
*/
public constructor(public source: ImageSource) {}

/**
* Whether this image has been resolved.
*/
public resolved() {
return this.#resolved != null;
}

/**
* Resolves this image to consumable form.
*/
public async resolve(): Promise<Image> {
if (this.#resolved) return this.#resolved;
return (this.#resolved = await createCanvasImage(this.source));
}
}

/**
* Creates a new template from the provided template.
* @param template The template to create from
*/
export const createTemplate = <F extends (...args: any[]) => any, P extends Parameters<F>>(
cb: (...args: P) => IImageGenerationTemplate
) => {
Expand All @@ -28,6 +46,9 @@ export const createTemplate = <F extends (...args: any[]) => any, P extends Para
};
};

/**
* The built-in template factory.
*/
export const TemplateFactory = {
Affect: createTemplate((image: ImageSource) => {
return {
Expand Down
3 changes: 3 additions & 0 deletions src/assets/fonts/fonts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* The bundled fonts in this package.
*/
export const Fonts = {
/**
* Geist sans font
Expand Down
8 changes: 7 additions & 1 deletion src/canvas/Canvacord.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ImageSource } from '../helpers';
import { ImageGen } from './ImageGen';
import { ImageGen, ImageGenerationTemplate } from './ImageGen';
import { buffer } from 'stream/consumers';
import type { Readable } from 'stream';
import { ImageFilterer } from './ImageFilterer';
Expand Down Expand Up @@ -112,4 +112,10 @@ Object.assign(CanvacordConstructor, factory);

export type Canvacord = CanvacordFactory & typeof CanvacordConstructor;

/**
* Creates a new Canvacord image processor.
* @param source The image source to use
* @param options The options to use
* @returns The image processor
*/
export const canvacord = CanvacordConstructor as Canvacord;
15 changes: 15 additions & 0 deletions src/canvas/CanvasHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@ import { Encodable } from './Encodable';
import { ContextManipulationStep } from './utils';

export abstract class CanvasHelper extends Encodable {
/**
* The steps to apply to the canvas.
*/
public steps: ContextManipulationStep[] = [];
private _canvas!: Canvas;

/**
* Creates a new CanvasHelper instance.
* @param width The width of the canvas
* @param height The height of the canvas
*/
public constructor(public width: number, public height: number) {
super();
}

/**
* Returns the canvas instance by applying the steps.
*/
public async getFinalCanvas(): Promise<Canvas> {
this._canvas ??= createCanvas(this.width, this.height);
const ctx = this._canvas.getContext('2d');
Expand All @@ -18,5 +30,8 @@ export abstract class CanvasHelper extends Encodable {
return this._canvas;
}

/**
* Processes the steps and applies them to the canvas.
*/
public abstract process(canvas: Canvas, ctx: SKRSContext2D): Promise<void>;
}
Loading