├── .npmignore
├── dist
├── types
│ ├── index.d.ts
│ ├── utils
│ │ ├── resizeImage.d.ts
│ │ ├── drawCanvasOriginal.d.ts
│ │ ├── drawCanvas.d.ts
│ │ ├── drawHoverCanvas.d.ts
│ │ ├── drawMousemoveCanvas.d.ts
│ │ ├── drawCanvasCircle.d.ts
│ │ ├── drawCanvasRoundSquare.d.ts
│ │ ├── averageColor.d.ts
│ │ └── averageLastPixelColor.d.ts
│ ├── types.d.ts
│ ├── Pixelator.d.ts
│ └── components
│ │ └── CanvasContainer.d.ts
└── interactive-pixelator.js
├── examples
└── demo-cra
│ ├── src
│ ├── react-app-env.d.ts
│ ├── components
│ │ ├── Color.jsx
│ │ ├── Grid.jsx
│ │ ├── GridColor.jsx
│ │ ├── Pixel.jsx
│ │ ├── DownloadButton.jsx
│ │ ├── FilterType.jsx
│ │ └── PixelType.jsx
│ ├── index.tsx
│ ├── index.css
│ ├── App.css
│ └── App.tsx
│ ├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── ralph-sample.jpg
│ ├── manifest.json
│ └── index.html
│ ├── config-overrides.js
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── package.json
│ └── README.md
├── lib
├── index.ts
├── types.ts
├── utils
│ ├── drawCanvasOriginal.ts
│ ├── resizeImage.ts
│ ├── averageColor.ts
│ ├── averageLastPixelColor.ts
│ ├── drawCanvasCircle.ts
│ ├── drawMousemoveCanvas.ts
│ ├── drawHoverCanvas.ts
│ ├── drawCanvasRoundSquare.ts
│ └── drawCanvas.ts
├── Pixelator.ts
└── components
│ └── CanvasContainer.ts
├── .babelrc
├── images
├── filter.gif
├── drawing1.gif
├── gridcolor1.gif
├── gridsize1.gif
├── pixelsize1.gif
└── pixeltype1.gif
├── .gitignore
├── tsconfig.json
├── rollup.config.js
├── LICENSE
├── package.json
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | examples/
3 | images/
--------------------------------------------------------------------------------
/dist/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import Pixelator from "./Pixelator";
2 | export { Pixelator };
3 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | import Pixelator from "./Pixelator";
2 |
3 | export { Pixelator };
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/plugin-proposal-class-properties"]
3 | }
4 |
--------------------------------------------------------------------------------
/images/filter.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/images/filter.gif
--------------------------------------------------------------------------------
/images/drawing1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/images/drawing1.gif
--------------------------------------------------------------------------------
/images/gridcolor1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/images/gridcolor1.gif
--------------------------------------------------------------------------------
/images/gridsize1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/images/gridsize1.gif
--------------------------------------------------------------------------------
/images/pixelsize1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/images/pixelsize1.gif
--------------------------------------------------------------------------------
/images/pixeltype1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/images/pixeltype1.gif
--------------------------------------------------------------------------------
/examples/demo-cra/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/dist/types/utils/resizeImage.d.ts:
--------------------------------------------------------------------------------
1 | export declare const resizeImage: (image: HTMLImageElement, $target: HTMLElement) => number[];
2 |
--------------------------------------------------------------------------------
/examples/demo-cra/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/examples/demo-cra/public/favicon.ico
--------------------------------------------------------------------------------
/examples/demo-cra/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/examples/demo-cra/public/logo192.png
--------------------------------------------------------------------------------
/examples/demo-cra/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/examples/demo-cra/public/logo512.png
--------------------------------------------------------------------------------
/examples/demo-cra/public/ralph-sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taenykim/interactive-pixelator/HEAD/examples/demo-cra/public/ralph-sample.jpg
--------------------------------------------------------------------------------
/dist/types/utils/drawCanvasOriginal.d.ts:
--------------------------------------------------------------------------------
1 | export declare const drawCanvasOriginal: (canvas: HTMLCanvasElement, image: HTMLImageElement, filterType: string) => void;
2 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/Color.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Color = () => {
4 | return
;
5 | };
6 |
7 | export default Color;
8 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | export interface PixelatorOptions {
2 | pixelSize?: number;
3 | gridSize?: number;
4 | gridColor?: string;
5 | pixelType?: string;
6 | filterType?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/dist/types/types.d.ts:
--------------------------------------------------------------------------------
1 | export interface PixelatorOptions {
2 | pixelSize?: number;
3 | gridSize?: number;
4 | gridColor?: string;
5 | pixelType?: string;
6 | filterType?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/dist/types/utils/drawCanvas.d.ts:
--------------------------------------------------------------------------------
1 | export declare const drawCanvas: (canvas: HTMLCanvasElement, image: HTMLImageElement, pixelSize: number, gridSize: number, gridColor: string, filterType: string) => void;
2 |
--------------------------------------------------------------------------------
/dist/types/utils/drawHoverCanvas.d.ts:
--------------------------------------------------------------------------------
1 | export declare const drawHoverCanvas: (canvas: HTMLCanvasElement, pixelSize: number, gridSize: number, y: number, x: number, hoverColor: string) => ImageData | null;
2 |
--------------------------------------------------------------------------------
/dist/types/utils/drawMousemoveCanvas.d.ts:
--------------------------------------------------------------------------------
1 | export declare const drawMousemoveCanvas: (canvas: HTMLCanvasElement, pixelSize: number, gridSize: number, y: number, x: number, gridColor: string) => ImageData | null;
2 |
--------------------------------------------------------------------------------
/dist/types/utils/drawCanvasCircle.d.ts:
--------------------------------------------------------------------------------
1 | export declare const drawCanvasCircle: (canvas: HTMLCanvasElement, image: HTMLImageElement, pixelSize: number, gridSize: number, gridColor: string, filterType: string) => void;
2 |
--------------------------------------------------------------------------------
/dist/types/utils/drawCanvasRoundSquare.d.ts:
--------------------------------------------------------------------------------
1 | export declare const drawCanvasRoundSquare: (canvas: HTMLCanvasElement, image: HTMLImageElement, pixelSize: number, gridSize: number, gridColor: string, filterType: string) => void;
2 |
--------------------------------------------------------------------------------
/dist/types/utils/averageColor.d.ts:
--------------------------------------------------------------------------------
1 | export declare const averageColor: (row: number, column: number, ctx: CanvasRenderingContext2D, tileSize: number, dataOffset: number) => {
2 | r: number;
3 | g: number;
4 | b: number;
5 | };
6 |
--------------------------------------------------------------------------------
/dist/types/utils/averageLastPixelColor.d.ts:
--------------------------------------------------------------------------------
1 | export declare const averageLastPixelColor: (row: number, column: number, ctx: CanvasRenderingContext2D, tileSize: number, dataOffset: number) => {
2 | r: number;
3 | g: number;
4 | b: number;
5 | };
6 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/Grid.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Grid = ({ gridSize, updateGridSize }) => {
4 | return updateGridSize(e)} min={0} max={10} />;
5 | };
6 |
7 | export default Grid;
8 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/GridColor.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const GridColor = ({ gridColor, updateGridColor }) => {
4 | return updateGridColor(e)} />;
5 | };
6 |
7 | export default GridColor;
8 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/Pixel.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Pixel = ({ pixelSize, updatePixelSize }) => {
4 | return updatePixelSize(e)} min={10} max={140} />;
5 | };
6 |
7 | export default Pixel;
8 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root"),
11 | );
12 |
--------------------------------------------------------------------------------
/examples/demo-cra/config-overrides.js:
--------------------------------------------------------------------------------
1 | const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
2 |
3 | module.exports = function override(config, env) {
4 | config.resolve.plugins = config.resolve.plugins.filter((plugin) => !(plugin instanceof ModuleScopePlugin));
5 |
6 | return config;
7 | };
8 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/DownloadButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const DownloadButton = ({ downloadHandler }) => {
4 | return (
5 |
8 | );
9 | };
10 |
11 | export default DownloadButton;
12 |
--------------------------------------------------------------------------------
/dist/types/Pixelator.d.ts:
--------------------------------------------------------------------------------
1 | import CanvasContainer from "./components/CanvasContainer";
2 | import { PixelatorOptions } from "./types";
3 | export default class Pixelator {
4 | $container: HTMLElement | null;
5 | $target: CanvasContainer | null;
6 | constructor(name: string, imageSrc: string, options?: PixelatorOptions);
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/demo-cra/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "skipLibCheck": true,
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | "declaration": true,
14 | "declarationDir": "dist/types"
15 | },
16 | "include": ["lib"]
17 | }
18 |
--------------------------------------------------------------------------------
/lib/utils/drawCanvasOriginal.ts:
--------------------------------------------------------------------------------
1 | export const drawCanvasOriginal = (canvas: HTMLCanvasElement, image: HTMLImageElement, filterType: string) => {
2 | const ctx = canvas.getContext("2d");
3 |
4 | if (ctx) {
5 | ctx.clearRect(0, 0, canvas.width, canvas.height);
6 | if (filterType === "grayscale") {
7 | ctx.filter = "grayscale(100%)";
8 | } else if (filterType === "invert") {
9 | ctx.filter = "invert(100%)";
10 | }
11 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/examples/demo-cra/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/demo-cra/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "jsx": "react",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "declaration": true,
15 | "declarationDir": "dist/types",
16 | "allowJs": true,
17 | "isolatedModules": true,
18 | "noEmit": true
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/lib/utils/resizeImage.ts:
--------------------------------------------------------------------------------
1 | export const resizeImage = (image: HTMLImageElement, $target: HTMLElement) => {
2 | let MAX_WIDTH = $target.getBoundingClientRect().width - 1;
3 | let MAX_HEIGHT = $target.getBoundingClientRect().height - 1;
4 | let width = image.width;
5 | let height = image.height;
6 |
7 | const artboardRatio = MAX_WIDTH / MAX_HEIGHT;
8 | const imageRatio = width / height;
9 |
10 | if (artboardRatio > imageRatio) {
11 | width *= MAX_HEIGHT / height;
12 | height = MAX_HEIGHT;
13 | } else {
14 | height *= MAX_WIDTH / width;
15 | width = MAX_WIDTH;
16 | }
17 | return [width, height];
18 | };
19 |
--------------------------------------------------------------------------------
/dist/types/components/CanvasContainer.d.ts:
--------------------------------------------------------------------------------
1 | import { PixelatorOptions } from "../types";
2 | export default class CanvasContainer {
3 | isDrawing: boolean;
4 | pixelSize: number;
5 | gridSize: number;
6 | gridColor: string;
7 | pixelType: string;
8 | filterType: string;
9 | canvas: HTMLCanvasElement;
10 | canvasFirstData: ImageData | null;
11 | constructor({ name, $container, image, options }: {
12 | name: string;
13 | $container: HTMLElement;
14 | image: HTMLImageElement;
15 | options: PixelatorOptions;
16 | });
17 | render($container: HTMLElement, canvas: HTMLCanvasElement): void;
18 | mousedownHandler(e: MouseEvent): void;
19 | mousemoveHandler(e: MouseEvent): void;
20 | mouseleaveHandler(): void;
21 | }
22 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from "rollup-plugin-commonjs";
2 | import resolve from "rollup-plugin-node-resolve";
3 | import babel from "rollup-plugin-babel";
4 | import pkg from "./package.json";
5 | import svgr from "@svgr/rollup";
6 | import url from "rollup-plugin-url";
7 | import typescript from "rollup-plugin-typescript";
8 |
9 | const extensions = [".js", ".ts"];
10 |
11 | export default {
12 | input: "./lib/index.ts",
13 | plugins: [
14 | resolve({ extensions }),
15 | babel({ extensions, include: ["src/**/*"], runtimeHelpers: true }),
16 | commonjs({
17 | include: "node_modules/**",
18 | }),
19 | url(),
20 | svgr(),
21 | typescript(),
22 | ],
23 | output: [
24 | {
25 | file: pkg.module,
26 | format: "es",
27 | },
28 | ],
29 | };
30 |
--------------------------------------------------------------------------------
/lib/Pixelator.ts:
--------------------------------------------------------------------------------
1 | import CanvasContainer from "./components/CanvasContainer";
2 | import { PixelatorOptions } from "./types";
3 |
4 | export default class Pixelator {
5 | $container: HTMLElement | null;
6 | $target: CanvasContainer | null;
7 | constructor(name: string, imageSrc: string, options: PixelatorOptions = {}) {
8 | const $container = document.getElementById(`${name}`);
9 | this.$container = $container;
10 | this.$target = null;
11 |
12 | const img = new Image();
13 | img.src = imageSrc;
14 | if ($container)
15 | img.addEventListener("load", () => {
16 | const image = img;
17 | this.$target = new CanvasContainer({
18 | name,
19 | $container,
20 | image,
21 | options,
22 | });
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/utils/averageColor.ts:
--------------------------------------------------------------------------------
1 | export const averageColor = (row: number, column: number, ctx: CanvasRenderingContext2D, tileSize: number, dataOffset: number) => {
2 | const rgb = {
3 | r: 0,
4 | g: 0,
5 | b: 0,
6 | };
7 | let data: ImageData;
8 |
9 | try {
10 | data = ctx.getImageData(column * tileSize, row * tileSize, tileSize, tileSize);
11 | } catch (e) {
12 | return rgb;
13 | }
14 |
15 | const length = data.data.length;
16 | let count = 0;
17 |
18 | for (let i = 0; i < length; i += dataOffset, count++) {
19 | rgb.r += data.data[i];
20 | rgb.g += data.data[i + 1];
21 | rgb.b += data.data[i + 2];
22 | }
23 |
24 | rgb.r = Math.floor(rgb.r / count);
25 | rgb.g = Math.floor(rgb.g / count);
26 | rgb.b = Math.floor(rgb.b / count);
27 |
28 | return rgb;
29 | };
30 |
--------------------------------------------------------------------------------
/lib/utils/averageLastPixelColor.ts:
--------------------------------------------------------------------------------
1 | const sizeOffset = 10;
2 |
3 | export const averageLastPixelColor = (row: number, column: number, ctx: CanvasRenderingContext2D, tileSize: number, dataOffset: number) => {
4 | const rgb = {
5 | r: 0,
6 | g: 0,
7 | b: 0,
8 | };
9 | let data: ImageData;
10 |
11 | try {
12 | data = ctx.getImageData(column * tileSize, row * tileSize, sizeOffset, sizeOffset);
13 | } catch (e) {
14 | return rgb;
15 | }
16 |
17 | const length = data.data.length;
18 | let count = 0;
19 |
20 | for (let i = 0; i < length; i += dataOffset, count++) {
21 | rgb.r += data.data[i];
22 | rgb.g += data.data[i + 1];
23 | rgb.b += data.data[i + 2];
24 | }
25 |
26 | rgb.r = Math.floor(rgb.r / count);
27 | rgb.g = Math.floor(rgb.g / count);
28 | rgb.b = Math.floor(rgb.b / count);
29 |
30 | return rgb;
31 | };
32 |
--------------------------------------------------------------------------------
/examples/demo-cra/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "0.1.2",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "interactive-pixelator": "0.1.2",
10 | "react": "^16.13.1",
11 | "react-dom": "^16.13.1",
12 | "react-scripts": "3.4.1"
13 | },
14 | "scripts": {
15 | "start": "react-app-rewired start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | },
35 | "devDependencies": {
36 | "@types/react": "^16.9.36",
37 | "@types/react-dom": "^16.9.8",
38 | "react-app-rewired": "^2.1.6",
39 | "typescript": "^3.9.5"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Taeeun Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/FilterType.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useEffect } from "react";
3 |
4 | const FilterType = ({ id, filterType, updateFilterType }) => {
5 | useEffect(() => {
6 | const container = document.getElementById(`filterType-radio-container${id}`);
7 | const matches = container.querySelectorAll("input");
8 | for (let i = 0; i < matches.length; i++) {
9 | if (matches[i].value === filterType) {
10 | matches[i].setAttribute("checked", true);
11 | return;
12 | }
13 | }
14 | }, [filterType]);
15 | return (
16 |
26 | );
27 | };
28 |
29 | export default FilterType;
30 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/components/PixelType.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useEffect } from "react";
3 |
4 | const PixelType = ({ id, pixelType, updatePixelType }) => {
5 | useEffect(() => {
6 | const container = document.getElementById(`pixeltype-radio-container${id}`);
7 | const matches = container.querySelectorAll("input");
8 | for (let i = 0; i < matches.length; i++) {
9 | if (matches[i].value === pixelType) {
10 | matches[i].setAttribute("checked", true);
11 | return;
12 | }
13 | }
14 | }, [pixelType]);
15 | return (
16 |
28 | );
29 | };
30 |
31 | export default PixelType;
32 |
--------------------------------------------------------------------------------
/examples/demo-cra/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/lib/utils/drawCanvasCircle.ts:
--------------------------------------------------------------------------------
1 | import { averageColor } from "./averageColor";
2 | import { averageLastPixelColor } from "./averageLastPixelColor";
3 |
4 | const dataOffset = 4; // we can set how many pixels to skip
5 |
6 | export const drawCanvasCircle = (canvas: HTMLCanvasElement, image: HTMLImageElement, pixelSize: number, gridSize: number, gridColor: string, filterType: string) => {
7 | gridColor = gridColor || "#000000";
8 | const ctx = canvas.getContext("2d");
9 | const tileSize = pixelSize;
10 | // const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
11 | const numTileRows = Math.ceil(canvas.height / tileSize);
12 | const numTileCols = Math.ceil(canvas.width / tileSize);
13 | if (ctx) ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
14 |
15 | // Loop through each tile
16 | for (let r = 0; r < numTileRows; r++) {
17 | for (let c = 0; c < numTileCols; c++) {
18 | // Set the pixel values for each tile
19 | let average;
20 | if (ctx) {
21 | if (c === numTileCols - 1 || r === numTileRows - 1) average = averageLastPixelColor(r, c, ctx, tileSize, dataOffset);
22 | else average = averageColor(r, c, ctx, tileSize, dataOffset);
23 | }
24 | const rgb = average;
25 | let red = rgb ? rgb.r : 0;
26 | let green = rgb ? rgb.g : 0;
27 | let blue = rgb ? rgb.b : 0;
28 |
29 | if (filterType === "invert") {
30 | red = 255 - red;
31 | green = 255 - green;
32 | blue = 255 - blue;
33 | } else if (filterType === "grayscale") {
34 | const gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
35 | red = gray;
36 | green = gray;
37 | blue = gray;
38 | }
39 |
40 | const trueRow = c * tileSize;
41 | const trueCol = r * tileSize;
42 | const arcCenterX = trueRow;
43 | const arcCenterY = trueCol;
44 | if (ctx) {
45 | ctx.beginPath();
46 | ctx.fillStyle = `${gridColor}`;
47 | ctx.fillRect(trueRow, trueCol, tileSize, tileSize);
48 | ctx.fillStyle = `rgb(${red},${green},${blue})`;
49 | ctx.arc(arcCenterX + tileSize / 2, arcCenterY + tileSize / 2, (tileSize - gridSize) / 2, 0, Math.PI * 2, false);
50 | ctx.fill();
51 | }
52 | }
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interactive-pixelator",
3 | "version": "0.1.2",
4 | "description": "upload image and make pixel art",
5 | "main": "dist/interactive-pixelator.js",
6 | "module": "dist/interactive-pixelator.js",
7 | "types": "dist/types/index.d.ts",
8 | "directories": {
9 | "example": "examples",
10 | "lib": "lib"
11 | },
12 | "scripts": {
13 | "storybook": "start-storybook -p 6006",
14 | "build-storybook": "build-storybook",
15 | "build": "rollup -c",
16 | "build:types": "tsc --emitDeclarationOnly"
17 | },
18 | "keywords": [
19 | "interactive",
20 | "pixelation",
21 | "pixelator",
22 | "image",
23 | "processing",
24 | "pixel",
25 | "art",
26 | "canvas",
27 | "image-processing",
28 | "javascript",
29 | "typescript",
30 | "web",
31 | "color"
32 | ],
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/taenykim/pixelator.git"
36 | },
37 | "author": "taenykim",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/taenykim/pixelator/issues"
41 | },
42 | "homepage": "https://github.com/taenykim/pixelator#readme",
43 | "devDependencies": {
44 | "@babel/core": "^7.10.2",
45 | "@babel/plugin-proposal-class-properties": "^7.10.1",
46 | "@babel/preset-env": "^7.10.2",
47 | "@babel/preset-react": "^7.10.1",
48 | "@svgr/rollup": "^5.4.0",
49 | "babel-loader": "^8.1.0",
50 | "babel-plugin-external-helpers": "^6.22.0",
51 | "babel-preset-es2015": "^6.24.1",
52 | "babel-preset-react": "^6.24.1",
53 | "css-loader": "^3.6.0",
54 | "rollup": "^2.16.1",
55 | "rollup-plugin-babel": "^4.4.0",
56 | "rollup-plugin-commonjs": "^10.1.0",
57 | "rollup-plugin-filesize": "^9.0.1",
58 | "rollup-plugin-json": "^4.0.0",
59 | "rollup-plugin-node-resolve": "^5.2.0",
60 | "rollup-plugin-peer-deps-external": "^2.2.2",
61 | "rollup-plugin-progress": "^1.1.2",
62 | "rollup-plugin-replace": "^2.2.0",
63 | "rollup-plugin-typescript": "^1.0.1",
64 | "rollup-plugin-url": "^3.0.1",
65 | "rollup-plugin-visualizer": "^4.0.4",
66 | "style-loader": "^1.2.1",
67 | "tslib": "^2.0.0",
68 | "typescript": "^3.9.5"
69 | },
70 | "peerDependencies": {
71 | "react": "^16.13.1",
72 | "react-dom": "^16.13.1"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/utils/drawMousemoveCanvas.ts:
--------------------------------------------------------------------------------
1 | const dataOffset = 4; // we can set how many pixels to skip
2 |
3 | export const drawMousemoveCanvas = (canvas: HTMLCanvasElement, pixelSize: number, gridSize: number, y: number, x: number, gridColor: string) => {
4 | const tileSize = pixelSize;
5 | const numTileCols = Math.ceil(canvas.width / tileSize);
6 |
7 | const ctx = canvas.getContext("2d");
8 | const grid = gridSize;
9 | const imageData = ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
10 | const pixels = imageData ? imageData.data : null;
11 |
12 | const rowIndex = Math.floor(x / tileSize);
13 | const colIndex = Math.floor(y / tileSize);
14 |
15 | // Set the pixel values for each tile
16 | const gridRed = parseInt(gridColor.substr(1, 2), 16);
17 | const gridGreen = parseInt(gridColor.substr(3, 2), 16);
18 | const gridBlue = parseInt(gridColor.substr(5, 2), 16);
19 |
20 | if (colIndex === numTileCols - 1) {
21 | for (let tr = 0; tr < tileSize; tr++) {
22 | for (let tc = 0; tc < canvas.width - colIndex * tileSize; tc++) {
23 | // Calculate the true position of the tile pixel
24 | const trueRow = rowIndex * tileSize + tr;
25 | const trueCol = colIndex * tileSize + tc;
26 |
27 | // Calculate the position of the current pixel in the array
28 | const imageDataWidth = imageData ? imageData.width : 0;
29 | const position = trueRow * (imageDataWidth * dataOffset) + trueCol * dataOffset;
30 |
31 | // Assign the colour to each pixel
32 | if (pixels) {
33 | pixels[position + 0] = gridRed;
34 | pixels[position + 1] = gridGreen;
35 | pixels[position + 2] = gridBlue;
36 | pixels[position + 3] = 255;
37 | }
38 | }
39 | }
40 | } else {
41 | // Loop through each tile pixel
42 | for (let tr = 0; tr < tileSize; tr++) {
43 | for (let tc = 0; tc < tileSize; tc++) {
44 | // Calculate the true position of the tile pixel
45 | const trueRow = rowIndex * tileSize + tr;
46 | const trueCol = colIndex * tileSize + tc;
47 |
48 | // Calculate the position of the current pixel in the array
49 | const position = trueRow * (canvas.width * dataOffset) + trueCol * dataOffset;
50 |
51 | // Assign the colour to each pixel
52 | if (pixels) {
53 | pixels[position + 0] = gridRed;
54 | pixels[position + 1] = gridGreen;
55 | pixels[position + 2] = gridBlue;
56 | pixels[position + 3] = 255;
57 | }
58 | }
59 | }
60 | }
61 |
62 | // Draw image data to the canvas
63 | return imageData;
64 | };
65 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | display: flex;
3 | position: relative;
4 | justify-content: center;
5 | align-items: center;
6 | width: 100vw;
7 | height: 100vh;
8 | }
9 |
10 | #upload-wrapper {
11 | border: 1px solid black;
12 | width: 90%;
13 | height: 90%;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | #upload-wrapper2 {
21 | border: 1px solid black;
22 | width: 90%;
23 | height: 90%;
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: center;
27 | align-items: center;
28 | }
29 |
30 | .upload-label {
31 | position: relative;
32 | border: 1px solid black;
33 | width: 90%;
34 | height: 90%;
35 | display: flex;
36 | flex-direction: column;
37 | justify-content: center;
38 | align-items: center;
39 | cursor: pointer;
40 | }
41 |
42 | .upload-description {
43 | text-align: center;
44 | }
45 |
46 | .upload-imgBtn {
47 | width: 80%;
48 | opacity: 0.2;
49 | padding-bottom: 20px;
50 | margin-top: 20px;
51 | }
52 |
53 | #canvas-container1 {
54 | position: relative;
55 | border: 1px solid black;
56 | width: 90%;
57 | height: 90%;
58 | display: flex;
59 | flex-direction: column;
60 | justify-content: center;
61 | align-items: center;
62 | cursor: pointer;
63 | }
64 |
65 | #canvas-container2 {
66 | position: relative;
67 | border: 1px solid black;
68 | width: 90%;
69 | height: 90%;
70 | display: flex;
71 | flex-direction: column;
72 | justify-content: center;
73 | align-items: center;
74 | cursor: pointer;
75 | }
76 |
77 | .img-item {
78 | margin-top: 10px;
79 | box-shadow: 0 14px 24px 0 rgba(186, 188, 191, 0.5);
80 | }
81 |
82 | .controller {
83 | position: absolute;
84 | display: flex;
85 | flex-direction: column;
86 | top: 10px;
87 | left: 10px;
88 | width: 200px;
89 | height: 300px;
90 | z-index: 10;
91 | background-color: azure;
92 | border: 2px solid rgb(0, 139, 139);
93 | text-align: center;
94 | }
95 |
96 | .contoller-hide-button {
97 | position: absolute;
98 | top: 10px;
99 | right: 10px;
100 | width: 100px;
101 | height: 100px;
102 | border-radius: 50%;
103 | z-index: 10;
104 | background: rgba(0, 139, 139, 0.5);
105 | }
106 |
107 | #color {
108 | width: 100%;
109 | height: 30px;
110 | font-size: 12px;
111 | text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
112 | color: white;
113 | border-top: 1px solid darkcyan;
114 | border-bottom: 1px solid darkcyan;
115 | }
116 |
117 | #pixeltype-radio-container {
118 | text-align: initial;
119 | }
120 |
121 | #main {
122 | display: flex;
123 | flex-direction: column;
124 | }
125 |
--------------------------------------------------------------------------------
/lib/utils/drawHoverCanvas.ts:
--------------------------------------------------------------------------------
1 | const dataOffset = 4; // we can set how many pixels to skip
2 |
3 | export const drawHoverCanvas = (canvas: HTMLCanvasElement, pixelSize: number, gridSize: number, y: number, x: number, hoverColor: string) => {
4 | const tileSize = pixelSize;
5 | const numTileCols = Math.ceil(canvas.width / tileSize);
6 |
7 | const ctx = canvas.getContext("2d");
8 | const grid = gridSize;
9 | const imageData = ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
10 | const pixels = imageData ? imageData.data : null;
11 |
12 | const rowIndex = Math.floor(x / tileSize);
13 | const colIndex = Math.floor(y / tileSize);
14 |
15 | // Set the pixel values for each tile
16 | const gridRed = 255 - parseInt(hoverColor.substr(1, 2), 16);
17 | const gridGreen = 255 - parseInt(hoverColor.substr(3, 2), 16);
18 | const gridBlue = 255 - parseInt(hoverColor.substr(5, 2), 16);
19 |
20 | if (colIndex === numTileCols - 1) {
21 | for (let tr = 0; tr < tileSize; tr++) {
22 | for (let tc = 0; tc < canvas.width - colIndex * tileSize; tc++) {
23 | // Calculate the true position of the tile pixel
24 | const trueRow = rowIndex * tileSize + tr;
25 | const trueCol = colIndex * tileSize + tc;
26 |
27 | // Calculate the position of the current pixel in the array
28 | const imageDataWidth = imageData ? imageData.width : 0;
29 | const position = trueRow * (imageDataWidth * dataOffset) + trueCol * dataOffset;
30 |
31 | // Assign the colour to each pixel
32 |
33 | if (pixels) {
34 | if (tc < grid || tr < grid || tc > canvas.width - colIndex * tileSize - grid || tr > canvas.height - rowIndex * tileSize - grid) {
35 | pixels[position + 0] = gridRed;
36 | pixels[position + 1] = gridGreen;
37 | pixels[position + 2] = gridBlue;
38 | pixels[position + 3] = 255;
39 | }
40 | }
41 | }
42 | }
43 | } else {
44 | // Loop through each tile pixel
45 | for (let tr = 0; tr < tileSize; tr++) {
46 | for (let tc = 0; tc < tileSize; tc++) {
47 | // Calculate the true position of the tile pixel
48 | const trueRow = rowIndex * tileSize + tr;
49 | const trueCol = colIndex * tileSize + tc;
50 |
51 | // Calculate the position of the current pixel in the array
52 | const position = trueRow * (canvas.width * dataOffset) + trueCol * dataOffset;
53 |
54 | // Assign the colour to each pixel
55 | if (pixels) {
56 | if (tc < grid || tr < grid || tr > canvas.height - rowIndex * tileSize - grid) {
57 | pixels[position + 0] = gridRed;
58 | pixels[position + 1] = gridGreen;
59 | pixels[position + 2] = gridBlue;
60 | pixels[position + 3] = 255;
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | // Draw image data to the canvas
68 | return imageData;
69 | };
70 |
--------------------------------------------------------------------------------
/examples/demo-cra/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/lib/utils/drawCanvasRoundSquare.ts:
--------------------------------------------------------------------------------
1 | import { resizeImage } from "./resizeImage";
2 | import { averageColor } from "./averageColor";
3 | import { averageLastPixelColor } from "./averageLastPixelColor";
4 |
5 | const dataOffset = 4; // we can set how many pixels to skip
6 |
7 | export const drawCanvasRoundSquare = (canvas: HTMLCanvasElement, image: HTMLImageElement, pixelSize: number, gridSize: number, gridColor: string, filterType: string) => {
8 | gridColor = gridColor || "#000000";
9 | const ctx = canvas.getContext("2d");
10 | const tileSize = pixelSize;
11 | const numTileRows = Math.ceil(canvas.height / tileSize);
12 | const numTileCols = Math.ceil(canvas.width / tileSize);
13 | if (ctx) ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
14 | const grid = Number(gridSize);
15 |
16 | // Loop through each tile
17 | for (let r = 0; r < numTileRows; r++) {
18 | for (let c = 0; c < numTileCols; c++) {
19 | // Set the pixel values for each tile
20 | let average;
21 | if (ctx) {
22 | if (c === numTileCols - 1 || r === numTileRows - 1) average = averageLastPixelColor(r, c, ctx, tileSize, dataOffset);
23 | else average = averageColor(r, c, ctx, tileSize, dataOffset);
24 | }
25 | const rgb = average;
26 | let red = rgb ? rgb.r : 0;
27 | let green = rgb ? rgb.g : 0;
28 | let blue = rgb ? rgb.b : 0;
29 |
30 | if (filterType === "invert") {
31 | red = 255 - red;
32 | green = 255 - green;
33 | blue = 255 - blue;
34 | } else if (filterType === "grayscale") {
35 | const gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
36 | red = gray;
37 | green = gray;
38 | blue = gray;
39 | }
40 |
41 | const trueRow = c * tileSize;
42 | const trueCol = r * tileSize;
43 | if (ctx) {
44 | ctx.beginPath();
45 | ctx.fillStyle = `${gridColor}`;
46 | ctx.fillRect(trueRow, trueCol, tileSize, tileSize);
47 | ctx.fillStyle = `rgb(${red},${green},${blue})`;
48 | ctx.strokeStyle = `rgb(${red},${green},${blue})`;
49 | const radius = (tileSize * 20) / 100;
50 | roundRect(ctx, trueRow + grid, trueCol + grid, tileSize - grid, tileSize - grid, radius, true);
51 | }
52 | }
53 | }
54 |
55 | function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, fill?: boolean, stroke?: boolean) {
56 | let _radius: any;
57 | if (typeof stroke === "undefined") {
58 | stroke = true;
59 | }
60 | if (typeof radius === "undefined") {
61 | radius = 5;
62 | }
63 | if (typeof radius === "number") {
64 | _radius = { tl: radius, tr: radius, br: radius, bl: radius };
65 | } else {
66 | const defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 };
67 | for (let side in defaultRadius) {
68 | _radius[side] = _radius[side] || 0;
69 | }
70 | }
71 | ctx.beginPath();
72 | ctx.moveTo(x + _radius.tl, y);
73 | ctx.lineTo(x + width - _radius.tr, y);
74 | ctx.quadraticCurveTo(x + width, y, x + width, y + _radius.tr);
75 | ctx.lineTo(x + width, y + height - _radius.br);
76 | ctx.quadraticCurveTo(x + width, y + height, x + width - _radius.br, y + height);
77 | ctx.lineTo(x + _radius.bl, y + height);
78 | ctx.quadraticCurveTo(x, y + height, x, y + height - _radius.bl);
79 | ctx.lineTo(x, y + _radius.tl);
80 | ctx.quadraticCurveTo(x, y, x + _radius.tl, y);
81 | ctx.closePath();
82 | if (fill) {
83 | ctx.fill();
84 | }
85 | if (stroke) {
86 | ctx.stroke();
87 | }
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Interactive pixelator
2 |
3 | 
4 | 
5 | 
6 | 
7 |
8 | 🌇 🌃 upload image and make interactive pixel art 🕹
9 |
10 | **[Demo page](https://interactive-pixelator.vercel.app) / [NPM page](https://www.npmjs.com/package/interactive-pixelator)**
11 |
12 | ## 🌟 Feature
13 |
14 |
15 |
16 | | Original Image |
17 | Change Pixel size |
18 |
19 |
20 |
21 |  |
22 |
23 |  |
24 |
25 |
26 | | Change Pixel type |
27 | Change Grid size |
28 |
29 |
30 |
31 |
32 | |
33 |
34 |
35 | |
36 |
37 |
38 | | Change Grid color |
39 | Draw freely |
40 |
41 |
42 |
43 |
44 | |
45 |
46 |
47 | |
48 |
49 |
50 | | Change filter |
51 | coming soon |
52 |
53 |
54 |
55 |
56 | |
57 |
58 | |
59 |
60 |
61 |
62 | ## 🌈 How to use
63 |
64 | ### 1. NPM install
65 |
66 | ```BASH
67 | $ npm i interactive-pixelator
68 | ```
69 |
70 | ### 2. Create DOM container
71 |
72 | > 🔥 Set width and height on the DOM element.
73 |
74 | ```html
75 |
76 | ```
77 |
78 | ### 3. And make Interactive Pixel Art!
79 |
80 | > If necessary, Add a controller, download button, etc. (reference : **[Demo page](https://interactive-pixelator.vercel.app)**)
81 |
82 | ```typescript
83 | import { Pixelator } from "interactive-pixelator";
84 |
85 | new Pixelator("DOM-element-id", "image URL", {
86 | // options
87 | pixelSize: number, // 1~ (default:100)
88 | gridSize: number, // 0~ (default:10)
89 | gridColor: string, // #000000 (default:#ffffff)
90 | pixelType: string, // [square(default), roundsquare, circle, original]
91 | filterType: string // [none(default), grayscale, invert]
92 | });
93 |
94 | //prettier-ignore
95 | ```
96 |
97 | ## 🏎 Demo play
98 |
99 | > I used create-react-app ! ☺️ [source code](https://github.com/taenykim/interactive-pixelator/tree/master/examples/demo-cra)
100 |
101 | ```BASH
102 | $ git clone https://github.com/taenykim/interactive-pixelator.git
103 | $ cd example/demo-cra
104 | $ npm install
105 | $ npm start
106 | ```
107 |
108 | ## ✏️ And..
109 |
110 | **This project is in progress.**
111 |
112 | **please give me a lot of advice and support.**
113 |
--------------------------------------------------------------------------------
/lib/components/CanvasContainer.ts:
--------------------------------------------------------------------------------
1 | import { drawCanvas } from "../utils/drawCanvas";
2 | import { drawCanvasCircle } from "../utils/drawCanvasCircle";
3 | import { drawCanvasOriginal } from "../utils/drawCanvasOriginal";
4 | import { drawCanvasRoundSquare } from "../utils/drawCanvasRoundSquare";
5 | import { resizeImage } from "../utils/resizeImage";
6 | import { drawMousemoveCanvas } from "../utils/drawMousemoveCanvas";
7 | import { drawHoverCanvas } from "../utils/drawHoverCanvas";
8 | import { PixelatorOptions } from "../types";
9 |
10 | export default class CanvasContainer {
11 | isDrawing: boolean;
12 | pixelSize: number;
13 | gridSize: number;
14 | gridColor: string;
15 | pixelType: string;
16 | filterType: string;
17 | canvas: HTMLCanvasElement;
18 | canvasFirstData: ImageData | null;
19 | constructor({ name, $container, image, options }: { name: string; $container: HTMLElement; image: HTMLImageElement; options: PixelatorOptions }) {
20 | this.isDrawing = false;
21 | this.pixelSize = options.pixelSize || 100;
22 | this.gridSize = options.gridSize || 10;
23 | this.gridColor = options.gridColor || "#ffffff";
24 | this.pixelType = options.pixelType || "square";
25 | this.filterType = options.filterType || "none";
26 | this.canvasFirstData = null;
27 |
28 | const canvas = document.createElement("canvas");
29 | this.canvas = canvas;
30 |
31 | $container.innerHTML = "";
32 | canvas.id = `${name}-canvas`;
33 | const [width, height] = resizeImage(image, $container);
34 | canvas.width = width;
35 | canvas.height = height;
36 | const ctx = canvas.getContext("2d");
37 |
38 | if (ctx) ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
39 |
40 | this.render($container, canvas);
41 |
42 | if (this.pixelType === "square") {
43 | drawCanvas(canvas, image, this.pixelSize, this.gridSize, this.gridColor, this.filterType);
44 | } else if (this.pixelType === "circle") {
45 | drawCanvasCircle(canvas, image, this.pixelSize, this.gridSize, this.gridColor, this.filterType);
46 | } else if (this.pixelType === "original") {
47 | drawCanvasOriginal(canvas, image, this.filterType);
48 | } else if (this.pixelType === "roundsquare") {
49 | drawCanvasRoundSquare(canvas, image, this.pixelSize, this.gridSize, this.gridColor, this.filterType);
50 | }
51 | if (ctx) this.canvasFirstData = ctx.getImageData(0, 0, canvas.width, canvas.height);
52 |
53 | canvas.addEventListener("mousedown", this.mousedownHandler.bind(this));
54 | canvas.addEventListener("mousemove", this.mousemoveHandler.bind(this));
55 | canvas.addEventListener("mouseleave", this.mouseleaveHandler.bind(this));
56 | window.addEventListener("mouseup", (e) => {
57 | this.isDrawing = false;
58 | });
59 | this.render($container, canvas);
60 | }
61 | render($container: HTMLElement, canvas: HTMLCanvasElement) {
62 | $container.innerHTML = "";
63 | $container.append(canvas);
64 | }
65 |
66 | mousedownHandler(e: MouseEvent) {
67 | const ctx = this.canvas.getContext("2d");
68 | this.isDrawing = true;
69 | const rect = this.canvas.getBoundingClientRect();
70 | const x = e.clientX - rect.left;
71 | const y = e.clientY - rect.top;
72 | this.canvasFirstData = drawMousemoveCanvas(this.canvas, this.pixelSize, this.gridSize, x, y, this.gridColor);
73 | if (ctx && this.canvasFirstData) ctx.putImageData(this.canvasFirstData, 0, 0);
74 | }
75 |
76 | mousemoveHandler(e: MouseEvent) {
77 | const ctx = this.canvas.getContext("2d");
78 | const rect = this.canvas.getBoundingClientRect();
79 | const x = e.clientX - rect.left;
80 | const y = e.clientY - rect.top;
81 | if (!this.isDrawing) {
82 | if (ctx && this.canvasFirstData) {
83 | ctx.putImageData(this.canvasFirstData, 0, 0);
84 | const dragHoveredImageData = drawHoverCanvas(this.canvas, this.pixelSize, this.gridSize, x, y, this.gridColor);
85 | if (dragHoveredImageData) ctx.putImageData(dragHoveredImageData, 0, 0);
86 | }
87 | return;
88 | }
89 | this.canvasFirstData = drawMousemoveCanvas(this.canvas, this.pixelSize, this.gridSize, x, y, this.gridColor);
90 | if (ctx && this.canvasFirstData) ctx.putImageData(this.canvasFirstData, 0, 0);
91 | }
92 | mouseleaveHandler() {
93 | const ctx = this.canvas.getContext("2d");
94 | if (ctx && this.canvasFirstData) ctx.putImageData(this.canvasFirstData, 0, 0);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/utils/drawCanvas.ts:
--------------------------------------------------------------------------------
1 | import { averageColor } from "./averageColor";
2 | import { averageLastPixelColor } from "./averageLastPixelColor";
3 | const dataOffset = 4; // we can set how many pixels to skip
4 | const borderSize = 0;
5 |
6 | export const drawCanvas = (canvas: HTMLCanvasElement, image: HTMLImageElement, pixelSize: number, gridSize: number, gridColor: string, filterType: string) => {
7 | gridColor = gridColor || "#000000";
8 | const ctx = canvas.getContext("2d");
9 | canvas.width = canvas.width - borderSize;
10 | canvas.height = canvas.height - borderSize;
11 | const tileSize = pixelSize;
12 | if (ctx) ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
13 | const imageData = ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
14 | const pixels = imageData ? imageData.data : null;
15 | const numTileRows = Math.ceil(canvas.height / tileSize);
16 | const numTileCols = Math.ceil(canvas.width / tileSize);
17 |
18 | const grid = gridSize;
19 | const gridRed = parseInt(gridColor.substr(1, 2), 16);
20 | const gridGreen = parseInt(gridColor.substr(3, 2), 16);
21 | const gridBlue = parseInt(gridColor.substr(5, 2), 16);
22 |
23 | // Loop through each tile
24 | for (let r = 0; r < numTileRows; r++) {
25 | for (let c = 0; c < numTileCols; c++) {
26 | // Set the pixel values for each tile
27 | let average;
28 | if (ctx) {
29 | if (c === numTileCols - 1 || r === numTileRows - 1) average = averageLastPixelColor(r, c, ctx, tileSize, dataOffset);
30 | else average = averageColor(r, c, ctx, tileSize, dataOffset);
31 | }
32 |
33 | const rgb = average;
34 | const red = rgb ? rgb.r : 0;
35 | const green = rgb ? rgb.g : 0;
36 | const blue = rgb ? rgb.b : 0;
37 |
38 | // Loop through each tile pixel
39 | if (c === numTileCols - 1) {
40 | for (let tr = 0; tr < tileSize; tr++) {
41 | for (let tc = 0; tc < canvas.width - c * tileSize; tc++) {
42 | // Calculate the true position of the tile pixel
43 | const trueRow = r * tileSize + tr;
44 | const trueCol = c * tileSize + tc;
45 |
46 | const imageDataWidth = imageData ? imageData.width : 0;
47 |
48 | // Calculate the position of the current pixel in the array
49 | const position = trueRow * (imageDataWidth * dataOffset) + trueCol * dataOffset;
50 |
51 | // console.log("position", position);
52 | // Assign the colour to each pixel
53 | if (pixels) {
54 | if (tc < grid || tr < grid || tc > canvas.width - c * tileSize - grid || tr > canvas.height - r * tileSize - grid) {
55 | pixels[position + 0] = gridRed;
56 | pixels[position + 1] = gridGreen;
57 | pixels[position + 2] = gridBlue;
58 | pixels[position + 3] = 255;
59 | } else {
60 | if (filterType === "invert") {
61 | pixels[position + 0] = 255 - red;
62 | pixels[position + 1] = 255 - green;
63 | pixels[position + 2] = 255 - blue;
64 | pixels[position + 3] = 255;
65 | } else if (filterType === "grayscale") {
66 | const gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
67 | pixels[position + 0] = gray;
68 | pixels[position + 1] = gray;
69 | pixels[position + 2] = gray;
70 | pixels[position + 3] = 255;
71 | } else {
72 | pixels[position + 0] = red;
73 | pixels[position + 1] = green;
74 | pixels[position + 2] = blue;
75 | pixels[position + 3] = 255;
76 | }
77 | }
78 | }
79 | }
80 | }
81 | } else {
82 | for (let tr = 0; tr < tileSize; tr++) {
83 | for (let tc = 0; tc < tileSize; tc++) {
84 | // Calculate the true position of the tile pixel
85 | const trueRow = r * tileSize + tr;
86 | const trueCol = c * tileSize + tc;
87 |
88 | const imageDataWidth = imageData ? imageData.width : 0;
89 |
90 | // Calculate the position of the current pixel in the array
91 | const position = trueRow * (imageDataWidth * dataOffset) + trueCol * dataOffset;
92 |
93 | // console.log("position", position);
94 | // Assign the colour to each pixel
95 | if (pixels) {
96 | if (tc < grid || tr < grid || tr > canvas.height - r * tileSize - grid) {
97 | pixels[position + 0] = gridRed;
98 | pixels[position + 1] = gridGreen;
99 | pixels[position + 2] = gridBlue;
100 | pixels[position + 3] = 255;
101 | } else {
102 | if (filterType === "invert") {
103 | pixels[position + 0] = 255 - red;
104 | pixels[position + 1] = 255 - green;
105 | pixels[position + 2] = 255 - blue;
106 | pixels[position + 3] = 255;
107 | } else if (filterType === "grayscale") {
108 | const gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
109 | pixels[position + 0] = gray;
110 | pixels[position + 1] = gray;
111 | pixels[position + 2] = gray;
112 | pixels[position + 3] = 255;
113 | } else {
114 | pixels[position + 0] = red;
115 | pixels[position + 1] = green;
116 | pixels[position + 2] = blue;
117 | pixels[position + 3] = 255;
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
127 | // Draw image data to the canvas
128 | if (ctx && imageData) ctx.putImageData(imageData, 0, 0);
129 | };
130 |
--------------------------------------------------------------------------------
/examples/demo-cra/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import "./App.css";
3 | import Color from "./components/Color";
4 | import Pixel from "./components/Pixel";
5 | import Grid from "./components/Grid";
6 | import GridColor from "./components/GridColor";
7 | import PixelType from "./components/PixelType";
8 | import DownloadButton from "./components/DownloadButton";
9 | import { Pixelator } from "interactive-pixelator";
10 | import FilterType from "./components/FilterType";
11 |
12 | const App = () => {
13 | const [pixelSize1, setPixelSize1] = useState(100);
14 | const [gridSize1, setGridSize1] = useState(5);
15 | const [gridColor1, setGridColor1] = useState("#ffffff");
16 | const [pixelType1, setPixelType1] = useState("square");
17 | const [filterType1, setFilterType1] = useState("none");
18 | const [pixelSize2, setPixelSize2] = useState(100);
19 | const [gridSize2, setGridSize2] = useState(5);
20 | const [gridColor2, setGridColor2] = useState("#ffffff");
21 | const [pixelType2, setPixelType2] = useState("square");
22 | const [filterType2, setFilterType2] = useState("none");
23 | const [imageSrc, setImageSrc] = useState("");
24 | const [controllerState1, setControllerState1] = useState(true);
25 | const [controllerState2, setControllerState2] = useState(true);
26 |
27 | const updatePixelSize1 = (e: any) => {
28 | setPixelSize1(e.target.value);
29 | };
30 |
31 | const updateGridSize1 = (e: any) => {
32 | setGridSize1(e.target.value);
33 | };
34 |
35 | const updateGridColor1 = (e: any) => {
36 | setGridColor1(e.target.value);
37 | };
38 |
39 | const updatePixelType1 = (type: string) => {
40 | setPixelType1(type);
41 | };
42 |
43 | const updateFilterType1 = (type: string) => {
44 | setFilterType1(type);
45 | };
46 |
47 | const downloadHandler1 = () => {
48 | const link = document.createElement("a");
49 | const canvas: any = document.getElementById("upload-wrapper-canvas");
50 | if (!canvas) return;
51 | link.download = "filename.png";
52 | link.href = canvas.toDataURL();
53 | link.click();
54 | };
55 | const updatePixelSize2 = (e: any) => {
56 | setPixelSize2(e.target.value);
57 | };
58 |
59 | const updateGridSize2 = (e: any) => {
60 | setGridSize2(e.target.value);
61 | };
62 |
63 | const updateGridColor2 = (e: any) => {
64 | setGridColor2(e.target.value);
65 | };
66 |
67 | const updatePixelType2 = (type: string) => {
68 | setPixelType2(type);
69 | };
70 |
71 | const updateFilterType2 = (type: string) => {
72 | setFilterType2(type);
73 | };
74 |
75 | const downloadHandler2 = () => {
76 | const link = document.createElement("a");
77 | const canvas: any = document.getElementById("upload-wrapper2-canvas");
78 | if (!canvas) return;
79 | link.download = "filename.png";
80 | link.href = canvas.toDataURL();
81 | link.click();
82 | };
83 |
84 | const hideButtonHandler = (where: number) => {
85 | const controller = document.getElementById(`controller${where}`);
86 | if (where === 1) {
87 | if (controller && controllerState1) {
88 | controller.style.display = `none`;
89 | } else if (controller && !controllerState1) {
90 | controller.style.display = `flex`;
91 | }
92 | setControllerState1(!controllerState1);
93 | } else if (where === 2) {
94 | if (controller && controllerState2) {
95 | controller.style.display = `none`;
96 | } else if (controller && !controllerState2) {
97 | controller.style.display = `flex`;
98 | }
99 | setControllerState2(!controllerState2);
100 | }
101 | console.log(controller);
102 | };
103 |
104 | useEffect(() => {
105 | new Pixelator("upload-wrapper2", imageSrc, { pixelSize: pixelSize2, gridSize: gridSize2, gridColor: gridColor2, pixelType: pixelType2, filterType: filterType2 });
106 | }, [imageSrc]);
107 |
108 | useEffect(() => {
109 | new Pixelator("upload-wrapper", "./ralph-sample.jpg", { pixelSize: pixelSize1, gridSize: gridSize1, gridColor: gridColor1, pixelType: pixelType1, filterType: filterType1 });
110 | new Pixelator("upload-wrapper2", imageSrc, { pixelSize: pixelSize2, gridSize: gridSize2, gridColor: gridColor2, pixelType: pixelType2, filterType: filterType2 });
111 | }, [pixelSize1, gridSize1, gridColor1, pixelType1, pixelSize2, gridSize2, gridColor2, pixelType2, filterType1, filterType2]);
112 |
113 | return (
114 |
115 |
116 |
125 |
Interactive Pixelator
126 |
🔽 scroll please ! 🔽
127 |
128 |
129 |
132 |
133 | color
134 |
135 | pixel size
136 |
137 | grid color
138 |
139 | grid size
140 |
141 | pixel type
142 |
143 | image filter
144 |
145 |
146 |
147 |
148 |
149 |
150 |
153 |
154 | color
155 |
156 | pixel size
157 |
158 | grid color
159 |
160 | grid size
161 |
162 | pixel type
163 |
164 | image filter
165 |
166 |
167 |
168 |
169 |
173 |
174 | {
181 | e.stopPropagation();
182 | const reader = new FileReader();
183 |
184 | reader.onload = (event: any) => {
185 | const img = new Image();
186 | const imgURL = event.target.result as string;
187 | img.src = imgURL;
188 |
189 | img.onload = () => {
190 | setImageSrc(img.src);
191 | };
192 | };
193 | reader.readAsDataURL(e.target.files[0]);
194 | }}
195 | />
196 |
197 |
198 |
199 |
200 | );
201 | };
202 |
203 | export default App;
204 |
--------------------------------------------------------------------------------
/dist/interactive-pixelator.js:
--------------------------------------------------------------------------------
1 | var averageColor = function (row, column, ctx, tileSize, dataOffset) {
2 | var rgb = {
3 | r: 0,
4 | g: 0,
5 | b: 0,
6 | };
7 | var data;
8 | try {
9 | data = ctx.getImageData(column * tileSize, row * tileSize, tileSize, tileSize);
10 | }
11 | catch (e) {
12 | return rgb;
13 | }
14 | var length = data.data.length;
15 | var count = 0;
16 | for (var i = 0; i < length; i += dataOffset, count++) {
17 | rgb.r += data.data[i];
18 | rgb.g += data.data[i + 1];
19 | rgb.b += data.data[i + 2];
20 | }
21 | rgb.r = Math.floor(rgb.r / count);
22 | rgb.g = Math.floor(rgb.g / count);
23 | rgb.b = Math.floor(rgb.b / count);
24 | return rgb;
25 | };
26 |
27 | var sizeOffset = 10;
28 | var averageLastPixelColor = function (row, column, ctx, tileSize, dataOffset) {
29 | var rgb = {
30 | r: 0,
31 | g: 0,
32 | b: 0,
33 | };
34 | var data;
35 | try {
36 | data = ctx.getImageData(column * tileSize, row * tileSize, sizeOffset, sizeOffset);
37 | }
38 | catch (e) {
39 | return rgb;
40 | }
41 | var length = data.data.length;
42 | var count = 0;
43 | for (var i = 0; i < length; i += dataOffset, count++) {
44 | rgb.r += data.data[i];
45 | rgb.g += data.data[i + 1];
46 | rgb.b += data.data[i + 2];
47 | }
48 | rgb.r = Math.floor(rgb.r / count);
49 | rgb.g = Math.floor(rgb.g / count);
50 | rgb.b = Math.floor(rgb.b / count);
51 | return rgb;
52 | };
53 |
54 | var dataOffset = 4; // we can set how many pixels to skip
55 | var borderSize = 0;
56 | var drawCanvas = function (canvas, image, pixelSize, gridSize, gridColor, filterType) {
57 | gridColor = gridColor || "#000000";
58 | var ctx = canvas.getContext("2d");
59 | canvas.width = canvas.width - borderSize;
60 | canvas.height = canvas.height - borderSize;
61 | var tileSize = pixelSize;
62 | if (ctx)
63 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
64 | var imageData = ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
65 | var pixels = imageData ? imageData.data : null;
66 | var numTileRows = Math.ceil(canvas.height / tileSize);
67 | var numTileCols = Math.ceil(canvas.width / tileSize);
68 | var grid = gridSize;
69 | var gridRed = parseInt(gridColor.substr(1, 2), 16);
70 | var gridGreen = parseInt(gridColor.substr(3, 2), 16);
71 | var gridBlue = parseInt(gridColor.substr(5, 2), 16);
72 | // Loop through each tile
73 | for (var r = 0; r < numTileRows; r++) {
74 | for (var c = 0; c < numTileCols; c++) {
75 | // Set the pixel values for each tile
76 | var average = void 0;
77 | if (ctx) {
78 | if (c === numTileCols - 1 || r === numTileRows - 1)
79 | average = averageLastPixelColor(r, c, ctx, tileSize, dataOffset);
80 | else
81 | average = averageColor(r, c, ctx, tileSize, dataOffset);
82 | }
83 | var rgb = average;
84 | var red = rgb ? rgb.r : 0;
85 | var green = rgb ? rgb.g : 0;
86 | var blue = rgb ? rgb.b : 0;
87 | // Loop through each tile pixel
88 | if (c === numTileCols - 1) {
89 | for (var tr = 0; tr < tileSize; tr++) {
90 | for (var tc = 0; tc < canvas.width - c * tileSize; tc++) {
91 | // Calculate the true position of the tile pixel
92 | var trueRow = r * tileSize + tr;
93 | var trueCol = c * tileSize + tc;
94 | var imageDataWidth = imageData ? imageData.width : 0;
95 | // Calculate the position of the current pixel in the array
96 | var position = trueRow * (imageDataWidth * dataOffset) + trueCol * dataOffset;
97 | // console.log("position", position);
98 | // Assign the colour to each pixel
99 | if (pixels) {
100 | if (tc < grid || tr < grid || tc > canvas.width - c * tileSize - grid || tr > canvas.height - r * tileSize - grid) {
101 | pixels[position + 0] = gridRed;
102 | pixels[position + 1] = gridGreen;
103 | pixels[position + 2] = gridBlue;
104 | pixels[position + 3] = 255;
105 | }
106 | else {
107 | if (filterType === "invert") {
108 | pixels[position + 0] = 255 - red;
109 | pixels[position + 1] = 255 - green;
110 | pixels[position + 2] = 255 - blue;
111 | pixels[position + 3] = 255;
112 | }
113 | else if (filterType === "grayscale") {
114 | var gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
115 | pixels[position + 0] = gray;
116 | pixels[position + 1] = gray;
117 | pixels[position + 2] = gray;
118 | pixels[position + 3] = 255;
119 | }
120 | else {
121 | pixels[position + 0] = red;
122 | pixels[position + 1] = green;
123 | pixels[position + 2] = blue;
124 | pixels[position + 3] = 255;
125 | }
126 | }
127 | }
128 | }
129 | }
130 | }
131 | else {
132 | for (var tr = 0; tr < tileSize; tr++) {
133 | for (var tc = 0; tc < tileSize; tc++) {
134 | // Calculate the true position of the tile pixel
135 | var trueRow = r * tileSize + tr;
136 | var trueCol = c * tileSize + tc;
137 | var imageDataWidth = imageData ? imageData.width : 0;
138 | // Calculate the position of the current pixel in the array
139 | var position = trueRow * (imageDataWidth * dataOffset) + trueCol * dataOffset;
140 | // console.log("position", position);
141 | // Assign the colour to each pixel
142 | if (pixels) {
143 | if (tc < grid || tr < grid || tr > canvas.height - r * tileSize - grid) {
144 | pixels[position + 0] = gridRed;
145 | pixels[position + 1] = gridGreen;
146 | pixels[position + 2] = gridBlue;
147 | pixels[position + 3] = 255;
148 | }
149 | else {
150 | if (filterType === "invert") {
151 | pixels[position + 0] = 255 - red;
152 | pixels[position + 1] = 255 - green;
153 | pixels[position + 2] = 255 - blue;
154 | pixels[position + 3] = 255;
155 | }
156 | else if (filterType === "grayscale") {
157 | var gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
158 | pixels[position + 0] = gray;
159 | pixels[position + 1] = gray;
160 | pixels[position + 2] = gray;
161 | pixels[position + 3] = 255;
162 | }
163 | else {
164 | pixels[position + 0] = red;
165 | pixels[position + 1] = green;
166 | pixels[position + 2] = blue;
167 | pixels[position + 3] = 255;
168 | }
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
175 | }
176 | // Draw image data to the canvas
177 | if (ctx && imageData)
178 | ctx.putImageData(imageData, 0, 0);
179 | };
180 |
181 | var dataOffset$1 = 4; // we can set how many pixels to skip
182 | var drawCanvasCircle = function (canvas, image, pixelSize, gridSize, gridColor, filterType) {
183 | gridColor = gridColor || "#000000";
184 | var ctx = canvas.getContext("2d");
185 | var tileSize = pixelSize;
186 | // const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
187 | var numTileRows = Math.ceil(canvas.height / tileSize);
188 | var numTileCols = Math.ceil(canvas.width / tileSize);
189 | if (ctx)
190 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
191 | // Loop through each tile
192 | for (var r = 0; r < numTileRows; r++) {
193 | for (var c = 0; c < numTileCols; c++) {
194 | // Set the pixel values for each tile
195 | var average = void 0;
196 | if (ctx) {
197 | if (c === numTileCols - 1 || r === numTileRows - 1)
198 | average = averageLastPixelColor(r, c, ctx, tileSize, dataOffset$1);
199 | else
200 | average = averageColor(r, c, ctx, tileSize, dataOffset$1);
201 | }
202 | var rgb = average;
203 | var red = rgb ? rgb.r : 0;
204 | var green = rgb ? rgb.g : 0;
205 | var blue = rgb ? rgb.b : 0;
206 | if (filterType === "invert") {
207 | red = 255 - red;
208 | green = 255 - green;
209 | blue = 255 - blue;
210 | }
211 | else if (filterType === "grayscale") {
212 | var gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
213 | red = gray;
214 | green = gray;
215 | blue = gray;
216 | }
217 | var trueRow = c * tileSize;
218 | var trueCol = r * tileSize;
219 | var arcCenterX = trueRow;
220 | var arcCenterY = trueCol;
221 | if (ctx) {
222 | ctx.beginPath();
223 | ctx.fillStyle = "" + gridColor;
224 | ctx.fillRect(trueRow, trueCol, tileSize, tileSize);
225 | ctx.fillStyle = "rgb(" + red + "," + green + "," + blue + ")";
226 | ctx.arc(arcCenterX + tileSize / 2, arcCenterY + tileSize / 2, (tileSize - gridSize) / 2, 0, Math.PI * 2, false);
227 | ctx.fill();
228 | }
229 | }
230 | }
231 | };
232 |
233 | var drawCanvasOriginal = function (canvas, image, filterType) {
234 | var ctx = canvas.getContext("2d");
235 | if (ctx) {
236 | ctx.clearRect(0, 0, canvas.width, canvas.height);
237 | if (filterType === "grayscale") {
238 | ctx.filter = "grayscale(100%)";
239 | }
240 | else if (filterType === "invert") {
241 | ctx.filter = "invert(100%)";
242 | }
243 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
244 | }
245 | };
246 |
247 | var dataOffset$2 = 4; // we can set how many pixels to skip
248 | var drawCanvasRoundSquare = function (canvas, image, pixelSize, gridSize, gridColor, filterType) {
249 | gridColor = gridColor || "#000000";
250 | var ctx = canvas.getContext("2d");
251 | var tileSize = pixelSize;
252 | var numTileRows = Math.ceil(canvas.height / tileSize);
253 | var numTileCols = Math.ceil(canvas.width / tileSize);
254 | if (ctx)
255 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
256 | var grid = Number(gridSize);
257 | // Loop through each tile
258 | for (var r = 0; r < numTileRows; r++) {
259 | for (var c = 0; c < numTileCols; c++) {
260 | // Set the pixel values for each tile
261 | var average = void 0;
262 | if (ctx) {
263 | if (c === numTileCols - 1 || r === numTileRows - 1)
264 | average = averageLastPixelColor(r, c, ctx, tileSize, dataOffset$2);
265 | else
266 | average = averageColor(r, c, ctx, tileSize, dataOffset$2);
267 | }
268 | var rgb = average;
269 | var red = rgb ? rgb.r : 0;
270 | var green = rgb ? rgb.g : 0;
271 | var blue = rgb ? rgb.b : 0;
272 | if (filterType === "invert") {
273 | red = 255 - red;
274 | green = 255 - green;
275 | blue = 255 - blue;
276 | }
277 | else if (filterType === "grayscale") {
278 | var gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
279 | red = gray;
280 | green = gray;
281 | blue = gray;
282 | }
283 | var trueRow = c * tileSize;
284 | var trueCol = r * tileSize;
285 | if (ctx) {
286 | ctx.beginPath();
287 | ctx.fillStyle = "" + gridColor;
288 | ctx.fillRect(trueRow, trueCol, tileSize, tileSize);
289 | ctx.fillStyle = "rgb(" + red + "," + green + "," + blue + ")";
290 | ctx.strokeStyle = "rgb(" + red + "," + green + "," + blue + ")";
291 | var radius = (tileSize * 20) / 100;
292 | roundRect(ctx, trueRow + grid, trueCol + grid, tileSize - grid, tileSize - grid, radius, true);
293 | }
294 | }
295 | }
296 | function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
297 | var _radius;
298 | if (typeof stroke === "undefined") {
299 | stroke = true;
300 | }
301 | if (typeof radius === "undefined") {
302 | radius = 5;
303 | }
304 | if (typeof radius === "number") {
305 | _radius = { tl: radius, tr: radius, br: radius, bl: radius };
306 | }
307 | else {
308 | var defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 };
309 | for (var side in defaultRadius) {
310 | _radius[side] = _radius[side] || 0;
311 | }
312 | }
313 | ctx.beginPath();
314 | ctx.moveTo(x + _radius.tl, y);
315 | ctx.lineTo(x + width - _radius.tr, y);
316 | ctx.quadraticCurveTo(x + width, y, x + width, y + _radius.tr);
317 | ctx.lineTo(x + width, y + height - _radius.br);
318 | ctx.quadraticCurveTo(x + width, y + height, x + width - _radius.br, y + height);
319 | ctx.lineTo(x + _radius.bl, y + height);
320 | ctx.quadraticCurveTo(x, y + height, x, y + height - _radius.bl);
321 | ctx.lineTo(x, y + _radius.tl);
322 | ctx.quadraticCurveTo(x, y, x + _radius.tl, y);
323 | ctx.closePath();
324 | if (fill) {
325 | ctx.fill();
326 | }
327 | if (stroke) {
328 | ctx.stroke();
329 | }
330 | }
331 | };
332 |
333 | var resizeImage = function (image, $target) {
334 | var MAX_WIDTH = $target.getBoundingClientRect().width - 1;
335 | var MAX_HEIGHT = $target.getBoundingClientRect().height - 1;
336 | var width = image.width;
337 | var height = image.height;
338 | var artboardRatio = MAX_WIDTH / MAX_HEIGHT;
339 | var imageRatio = width / height;
340 | if (artboardRatio > imageRatio) {
341 | width *= MAX_HEIGHT / height;
342 | height = MAX_HEIGHT;
343 | }
344 | else {
345 | height *= MAX_WIDTH / width;
346 | width = MAX_WIDTH;
347 | }
348 | return [width, height];
349 | };
350 |
351 | var dataOffset$3 = 4; // we can set how many pixels to skip
352 | var drawMousemoveCanvas = function (canvas, pixelSize, gridSize, y, x, gridColor) {
353 | var tileSize = pixelSize;
354 | var numTileCols = Math.ceil(canvas.width / tileSize);
355 | var ctx = canvas.getContext("2d");
356 | var imageData = ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
357 | var pixels = imageData ? imageData.data : null;
358 | var rowIndex = Math.floor(x / tileSize);
359 | var colIndex = Math.floor(y / tileSize);
360 | // Set the pixel values for each tile
361 | var gridRed = parseInt(gridColor.substr(1, 2), 16);
362 | var gridGreen = parseInt(gridColor.substr(3, 2), 16);
363 | var gridBlue = parseInt(gridColor.substr(5, 2), 16);
364 | if (colIndex === numTileCols - 1) {
365 | for (var tr = 0; tr < tileSize; tr++) {
366 | for (var tc = 0; tc < canvas.width - colIndex * tileSize; tc++) {
367 | // Calculate the true position of the tile pixel
368 | var trueRow = rowIndex * tileSize + tr;
369 | var trueCol = colIndex * tileSize + tc;
370 | // Calculate the position of the current pixel in the array
371 | var imageDataWidth = imageData ? imageData.width : 0;
372 | var position = trueRow * (imageDataWidth * dataOffset$3) + trueCol * dataOffset$3;
373 | // Assign the colour to each pixel
374 | if (pixels) {
375 | pixels[position + 0] = gridRed;
376 | pixels[position + 1] = gridGreen;
377 | pixels[position + 2] = gridBlue;
378 | pixels[position + 3] = 255;
379 | }
380 | }
381 | }
382 | }
383 | else {
384 | // Loop through each tile pixel
385 | for (var tr = 0; tr < tileSize; tr++) {
386 | for (var tc = 0; tc < tileSize; tc++) {
387 | // Calculate the true position of the tile pixel
388 | var trueRow = rowIndex * tileSize + tr;
389 | var trueCol = colIndex * tileSize + tc;
390 | // Calculate the position of the current pixel in the array
391 | var position = trueRow * (canvas.width * dataOffset$3) + trueCol * dataOffset$3;
392 | // Assign the colour to each pixel
393 | if (pixels) {
394 | pixels[position + 0] = gridRed;
395 | pixels[position + 1] = gridGreen;
396 | pixels[position + 2] = gridBlue;
397 | pixels[position + 3] = 255;
398 | }
399 | }
400 | }
401 | }
402 | // Draw image data to the canvas
403 | return imageData;
404 | };
405 |
406 | var dataOffset$4 = 4; // we can set how many pixels to skip
407 | var drawHoverCanvas = function (canvas, pixelSize, gridSize, y, x, hoverColor) {
408 | var tileSize = pixelSize;
409 | var numTileCols = Math.ceil(canvas.width / tileSize);
410 | var ctx = canvas.getContext("2d");
411 | var grid = gridSize;
412 | var imageData = ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
413 | var pixels = imageData ? imageData.data : null;
414 | var rowIndex = Math.floor(x / tileSize);
415 | var colIndex = Math.floor(y / tileSize);
416 | // Set the pixel values for each tile
417 | var gridRed = 255 - parseInt(hoverColor.substr(1, 2), 16);
418 | var gridGreen = 255 - parseInt(hoverColor.substr(3, 2), 16);
419 | var gridBlue = 255 - parseInt(hoverColor.substr(5, 2), 16);
420 | if (colIndex === numTileCols - 1) {
421 | for (var tr = 0; tr < tileSize; tr++) {
422 | for (var tc = 0; tc < canvas.width - colIndex * tileSize; tc++) {
423 | // Calculate the true position of the tile pixel
424 | var trueRow = rowIndex * tileSize + tr;
425 | var trueCol = colIndex * tileSize + tc;
426 | // Calculate the position of the current pixel in the array
427 | var imageDataWidth = imageData ? imageData.width : 0;
428 | var position = trueRow * (imageDataWidth * dataOffset$4) + trueCol * dataOffset$4;
429 | // Assign the colour to each pixel
430 | if (pixels) {
431 | if (tc < grid || tr < grid || tc > canvas.width - colIndex * tileSize - grid || tr > canvas.height - rowIndex * tileSize - grid) {
432 | pixels[position + 0] = gridRed;
433 | pixels[position + 1] = gridGreen;
434 | pixels[position + 2] = gridBlue;
435 | pixels[position + 3] = 255;
436 | }
437 | }
438 | }
439 | }
440 | }
441 | else {
442 | // Loop through each tile pixel
443 | for (var tr = 0; tr < tileSize; tr++) {
444 | for (var tc = 0; tc < tileSize; tc++) {
445 | // Calculate the true position of the tile pixel
446 | var trueRow = rowIndex * tileSize + tr;
447 | var trueCol = colIndex * tileSize + tc;
448 | // Calculate the position of the current pixel in the array
449 | var position = trueRow * (canvas.width * dataOffset$4) + trueCol * dataOffset$4;
450 | // Assign the colour to each pixel
451 | if (pixels) {
452 | if (tc < grid || tr < grid || tr > canvas.height - rowIndex * tileSize - grid) {
453 | pixels[position + 0] = gridRed;
454 | pixels[position + 1] = gridGreen;
455 | pixels[position + 2] = gridBlue;
456 | pixels[position + 3] = 255;
457 | }
458 | }
459 | }
460 | }
461 | }
462 | // Draw image data to the canvas
463 | return imageData;
464 | };
465 |
466 | var CanvasContainer = /** @class */ (function () {
467 | function CanvasContainer(_a) {
468 | var _this = this;
469 | var name = _a.name, $container = _a.$container, image = _a.image, options = _a.options;
470 | this.isDrawing = false;
471 | this.pixelSize = options.pixelSize || 100;
472 | this.gridSize = options.gridSize || 10;
473 | this.gridColor = options.gridColor || "#ffffff";
474 | this.pixelType = options.pixelType || "square";
475 | this.filterType = options.filterType || "none";
476 | this.canvasFirstData = null;
477 | var canvas = document.createElement("canvas");
478 | this.canvas = canvas;
479 | $container.innerHTML = "";
480 | canvas.id = name + "-canvas";
481 | var _b = resizeImage(image, $container), width = _b[0], height = _b[1];
482 | canvas.width = width;
483 | canvas.height = height;
484 | var ctx = canvas.getContext("2d");
485 | if (ctx)
486 | ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
487 | this.render($container, canvas);
488 | if (this.pixelType === "square") {
489 | drawCanvas(canvas, image, this.pixelSize, this.gridSize, this.gridColor, this.filterType);
490 | }
491 | else if (this.pixelType === "circle") {
492 | drawCanvasCircle(canvas, image, this.pixelSize, this.gridSize, this.gridColor, this.filterType);
493 | }
494 | else if (this.pixelType === "original") {
495 | drawCanvasOriginal(canvas, image, this.filterType);
496 | }
497 | else if (this.pixelType === "roundsquare") {
498 | drawCanvasRoundSquare(canvas, image, this.pixelSize, this.gridSize, this.gridColor, this.filterType);
499 | }
500 | if (ctx)
501 | this.canvasFirstData = ctx.getImageData(0, 0, canvas.width, canvas.height);
502 | canvas.addEventListener("mousedown", this.mousedownHandler.bind(this));
503 | canvas.addEventListener("mousemove", this.mousemoveHandler.bind(this));
504 | canvas.addEventListener("mouseleave", this.mouseleaveHandler.bind(this));
505 | window.addEventListener("mouseup", function (e) {
506 | _this.isDrawing = false;
507 | });
508 | this.render($container, canvas);
509 | }
510 | CanvasContainer.prototype.render = function ($container, canvas) {
511 | $container.innerHTML = "";
512 | $container.append(canvas);
513 | };
514 | CanvasContainer.prototype.mousedownHandler = function (e) {
515 | var ctx = this.canvas.getContext("2d");
516 | this.isDrawing = true;
517 | var rect = this.canvas.getBoundingClientRect();
518 | var x = e.clientX - rect.left;
519 | var y = e.clientY - rect.top;
520 | this.canvasFirstData = drawMousemoveCanvas(this.canvas, this.pixelSize, this.gridSize, x, y, this.gridColor);
521 | if (ctx && this.canvasFirstData)
522 | ctx.putImageData(this.canvasFirstData, 0, 0);
523 | };
524 | CanvasContainer.prototype.mousemoveHandler = function (e) {
525 | var ctx = this.canvas.getContext("2d");
526 | var rect = this.canvas.getBoundingClientRect();
527 | var x = e.clientX - rect.left;
528 | var y = e.clientY - rect.top;
529 | if (!this.isDrawing) {
530 | if (ctx && this.canvasFirstData) {
531 | ctx.putImageData(this.canvasFirstData, 0, 0);
532 | var dragHoveredImageData = drawHoverCanvas(this.canvas, this.pixelSize, this.gridSize, x, y, this.gridColor);
533 | if (dragHoveredImageData)
534 | ctx.putImageData(dragHoveredImageData, 0, 0);
535 | }
536 | return;
537 | }
538 | this.canvasFirstData = drawMousemoveCanvas(this.canvas, this.pixelSize, this.gridSize, x, y, this.gridColor);
539 | if (ctx && this.canvasFirstData)
540 | ctx.putImageData(this.canvasFirstData, 0, 0);
541 | };
542 | CanvasContainer.prototype.mouseleaveHandler = function () {
543 | var ctx = this.canvas.getContext("2d");
544 | if (ctx && this.canvasFirstData)
545 | ctx.putImageData(this.canvasFirstData, 0, 0);
546 | };
547 | return CanvasContainer;
548 | }());
549 |
550 | var Pixelator = /** @class */ (function () {
551 | function Pixelator(name, imageSrc, options) {
552 | var _this = this;
553 | if (options === void 0) { options = {}; }
554 | var $container = document.getElementById("" + name);
555 | this.$container = $container;
556 | this.$target = null;
557 | var img = new Image();
558 | img.src = imageSrc;
559 | if ($container)
560 | img.addEventListener("load", function () {
561 | var image = img;
562 | _this.$target = new CanvasContainer({
563 | name: name,
564 | $container: $container,
565 | image: image,
566 | options: options,
567 | });
568 | });
569 | }
570 | return Pixelator;
571 | }());
572 |
573 | export { Pixelator };
574 |
--------------------------------------------------------------------------------