<img src="https://github.com/EastSun5566/cc-gram/blob/main/logo.webp?raw=true" alt="CCgram Logo" width="180">

# 🖼 CCgram

[![NPM Version](https://img.shields.io/npm/v/cc-gram.svg?style=for-the-badge)](https://www.npmjs.com/package/cc-gram)
[![NPM Downloads](https://img.shields.io/npm/dt/cc-gram.svg?style=for-the-badge)](https://www.npmjs.com/package/cc-gram)
[![JSR Version](https://img.shields.io/jsr/v/%40eastsun5566/cc-gram?style=for-the-badge)](https://jsr.io/@eastsun5566/cc-gram)
[![Test Status](https://img.shields.io/github/actions/workflow/status/EastSun5566/cc-gram/test.yml?style=for-the-badge)](https://github.com/EastSun5566/cc-gram/actions/workflows/test.yml)
[![License](https://img.shields.io/github/license/EastSun5566/cc-gram.svg?style=for-the-badge)](https://github.com/EastSun5566/cc-gram/blob/main/LICENSE)

[<img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" height="40">](https://www.buymeacoffee.com/eastsun5566)

> A CSS & Canvas Instagram filter inspired by CSSgram

🔗 <https://eastsun5566.github.io/cc-gram/>

![Demo GIF](./demo.gif)

## 🤔 Why

> [CSSgram](https://github.com/una/CSSgram) is an excellent CSS filter library. However, there are instances where you might need to access or download the image with filter. This is where CCgram comes into play. It enables you to preview filter using pure CSS and draw them with Canvas whenever you need to.

- On-Demand: Utilizes CSS for previewing and draws with the Canvas API as needed
- Non-Blocking: When supported, images are drawn on a Web Worker using `OffscreenCanvas` & `ImageBitmap`; otherwise the library falls back to the main thread.

## ✨ Installation

```sh
npm i cc-gram
```

## 🚀 Usage

### Apply CSS filter

#### HTML

> An image tag with `data-filter` attribute is filter name

```html
<img src="./my-picture.png" data-filter="1977" />
```

#### JavaScript

> Initialize to apply CSS filter to All targets has `data-filter` attribute

```js
import { createFilter } from "cc-gram";

const filter = createFilter();
```

Each filter instance has its own filter list. Custom filters added or changed
through one instance do not affect other instances or the default presets.

```js
// or you can turn off init apply
const filter = createFilter({ init: false });

// you can also specify data attribute
// i.e., <img data-my-attr="1977">
const filter = createFilter({ dataAttribute: "my-attr" });
```

---

##### Manual apply CSS filter

> `applyFilter()`

```js
// apply to All targets has `data-filter` attribute
filter.applyFilter();

// or you can just use selector for performance
filter.applyFilter("#my-image");
```

##### All available filter name list

> `filterNames`

```js
const filterNames = filter.filterNames;
```

- Default filter Name list:

  - `aden`
  - `inkwell`
  - `reyes`
  - `gingham`
  - `toaster`
  - `walden`
  - `hudson`
  - `earlybird`
  - `mayfair`
  - `lofi`
  - `1977`
  - `brooklyn`
  - `xpro2`
  - `nashville`
  - `lark`
  - `moon`
  - `clarendon`
  - `willow`
  - `rise`
  - `slumber`
  - `brannan`
  - `valencia`
  - `maven`
  - `stinson`
  - `amaro`
  - `pink`
  - `vesper`
  - `sierra`
  - `helena`
  - `ginza`
  - `juno`
  - `skyline`
  - `ludwig`
  - `hefe`
  - `dogpatch`
  - `kelvin`
  - `sutro`
  - `ashby`
  - `crema`

##### Add / Set filter to the filter list

> `setFilter(name, setting)`

- name: `string` - The filter name
- setting: `object` - The filter setting

```js
filter.setFilter("my-filter", {
  saturate: 0.8,
  contrast: 1.2,
});
```

- Available setting key (all value is number):

  - `blur`
  - `brightness`
  - `contrast`
  - `grayscale`
  - `hue-rotate`
  - `invert`
  - `saturate`
  - `sepia`

##### Remove the filter from the filter list

> `removeFilter(name)`

- name: `string` - The filter name

```js
filter.removeFilter("my-filter");
```

---

### Access image with filter

```js
const image = document.querySelector('img[data-filter="1977"]');
```

#### Data URL

> `getDataURL(image[, options = {}])`

```js
const dataUrl = await filter.getDataURL(image);
```

#### Blob

> `getBlob(image[, options = {}])`

```js
const blob = await filter.getBlob(image, {
  type: "image/jpeg",
  quality: 0.8,
});
```

CSS previews and canvas exports have separate browser support requirements.
Filtered exports require the 2D canvas context used by the renderer (including
the Worker context when `OffscreenCanvas` is available) to support `filter`.
If it does not, `getBlob()` and `getDataURL()` reject with
`[CCgram] Canvas filters are not supported.` instead of silently exporting the
original image. Unfiltered exports still work without canvas filter support.
See [canvas filter compatibility](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter#browser_compatibility).

- Options

  - type: `string` - MIME types, defaults to `image/png`,
  - quality: `number` - [0 - 1], defaults to `0.92`
  - filter: `string` - Override filter name, defaults to reading from data attribute

##### Override filter

You can override the filter applied to the image by passing a `filter` option:

```js
// Image has data-filter="1977"
const image = document.querySelector('img[data-filter="1977"]');

// Apply a different filter when getting the image data
const dataUrl = await filter.getDataURL(image, { filter: "inkwell" });
const blob = await filter.getBlob(image, { filter: "valencia" });
```

#### Exporting cross-origin images

A cross-origin image can display with CSS filters without being eligible for
canvas export. For `getBlob()` or `getDataURL()`, request the image with CORS
enabled **before** loading it:

```js
import { createFilter } from "cc-gram";

const image = new Image();
image.crossOrigin = "anonymous"; // Set before src starts the request.
image.src = "https://images.example.com/photo.png";
await image.decode();

const filter = createFilter({ init: false });
const blob = await filter.getBlob(image, { filter: "1977" });
```

The image server must return an `Access-Control-Allow-Origin` header allowing
your page's origin (or `*` for publicly accessible images). Setting `crossOrigin`
alone does not grant permission; if the server disallows CORS, loading/decoding
the image fails.

Without CORS approval, drawing a cross-origin image taints the canvas, and export
can reject with `SecurityError` (or `DataCloneError` when transferring a tainted
image to a Worker). CCgram cannot bypass this browser protection.
Same-origin images and object URLs created from user-selected local `File`s do
not need cross-origin response headers. See
[MDN: Use cross-origin images in a canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/CORS_enabled_image).

## 🔧 Development

```sh
# install deps
pnpm i

# fix style
pnpm run lint

# run test
pnpm test

# build for prod
pnpm run build
```
