├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── build.js
├── package.json
└── src
├── area
├── functions.js
└── select.js
├── batch
├── README.md
├── filter
│ ├── blend.js
│ ├── invert.js
│ ├── onion.js
│ ├── replace.js
│ ├── shading.js
│ └── smoothing.js
├── index.js
├── matrix.js
└── tile.js
├── bounds
└── index.js
├── camera
└── functions.js
├── cfg.js
├── color.js
├── container
└── index.js
├── env
├── fill.js
├── functions.js
└── insert.js
├── event
├── emitter.js
└── listener.js
├── extend.js
├── index.js
├── layer
├── README.md
├── index.js
└── matrix.js
├── math.js
├── render
├── buffer.js
├── build.js
├── draw.js
├── generate.js
├── main.js
├── render.js
├── resize.js
└── shaders.js
├── setup.js
├── stack
├── cmd.js
├── kind.js
├── redo.js
├── state.js
└── undo.js
├── storage
├── read.js
└── write.js
├── transform
├── flip.js
└── rotate.js
├── ui
└── index.js
└── utils.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Line endings: enforce LF in GitHub, convert to native on checkout.
2 |
3 | * text=auto
4 | *.js text
5 |
6 | # Make GitHub ignore vendor libraries while computing language stats.
7 | # See https://github.com/github/linguist#overrides.
8 |
9 | *.proto linguist-vendored=true
10 | *.sh linguist-vendored=true
11 | *.bat linguist-vendored=true
12 | *.css linguist-vendored=true
13 | *.html linguist-vendored=true
14 |
15 | # Explicitly specify language for non-standard extensions used under
16 | # ide/web/lib/templates to make GitHub correctly count their language stats.
17 | #
18 | *.js_ linguist-language=JavaScript
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log
3 | npm-debug.log*
4 | node_modules
5 |
6 | benchmark
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2017, Felix Maier
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Poxi is a modern, flat pixel art editor for the browser with focus on elegance, simplicity and productivity.
4 |
5 |
6 |
7 |
26 |
27 | ### Engine features
28 | - Smart batching
29 | - WebGL-based renderer
30 | - Low-level matrices
31 | - Undo/Redo for all operations
32 | - Infinite grid
33 | - Copy by reference
34 |
35 | I've created this pixel editor because of the lack of smooth pixel editors inside the browser. All current implementations lack of speed and just feel clunky and slow. I've created a whole low-level pixel matrix framework from scratch for this, offering incredible speed, a undo/redo state machine and various basic transformation methods. This allows you to work on images even larger than 8000px á 8000px with very low memory consummation.
36 |
37 | ### Coming soon
38 | - Animations
39 | - Selections
40 | - Faster bucket filling
41 |
42 | ### Contributing
43 |
44 | Code related pull requests are very welcome, but please make sure they match the existing code style.
45 |
46 | ### License
47 | [BSD-2](https://github.com/maierfelix/poxi/blob/master/LICENSE)
48 |
--------------------------------------------------------------------------------
/build.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const url = require("url");
3 | const path = require("path");
4 | const buble = require("rollup-plugin-buble");
5 | const rollup = require("rollup");
6 | const electron = require("electron");
7 |
8 | const bundleSource = () => {
9 | return new Promise((resolve, reject) => {
10 | rollup.rollup({
11 | entry: __dirname + "/src/index.js",
12 | plugins: [ buble() ],
13 | }).then((bundle) => {
14 | const result = bundle.generate({
15 | format: "cjs"
16 | });
17 | const code = "(function() { " + result.code + "})();";
18 | fs.writeFileSync("static/bundle.js", code);
19 | resolve();
20 | }).catch((e) => {
21 | reject(e);
22 | });
23 | });
24 | };
25 |
26 | // now open up electron with our fresh rolluped bundle
27 | const initElectron = () => {
28 | return new Promise((resolve) => {
29 |
30 | const app = electron.app;
31 | const BrowserWindow = electron.BrowserWindow;
32 |
33 | let win = null;
34 | const createWindow = () => {
35 | win = new BrowserWindow({
36 | width: 980,
37 | height: 680,
38 | titleBarStyle: "hidden",
39 | icon: path.join(__dirname, "/static/assets/img/tree.png")
40 | });
41 |
42 | win.loadURL(url.format({
43 | pathname: path.join(__dirname, "/static/index.html"),
44 | protocol: "file:",
45 | slashes: true
46 | }));
47 | win.setMenu(null);
48 |
49 | //win.setFullScreen(true);
50 | win.webContents.openDevTools();
51 | win.on("closed", () => {
52 | win = null;
53 | });
54 | resolve({ win, app });
55 | };
56 |
57 | app.on("ready", createWindow);
58 | app.on("window-all-closed", () => {
59 | if (process.platform !== "darwin") app.quit();
60 | });
61 | app.on("activate", () => {
62 | if (win === null) createWindow();
63 | });
64 |
65 | });
66 | };
67 |
68 | const initializeStage = () => {
69 | return new Promise((resolve) => {
70 | bundleSource().then(() => {
71 | initElectron().then(resolve);
72 | }).catch((e) => {
73 | throw new Error(e);
74 | });
75 | });
76 | };
77 |
78 | // simple live reload system
79 | initializeStage().then((old) => {
80 | const onRefresh = (e, file) => {
81 | // prevent circular tracking
82 | if (file === "bundle.js") return;
83 | bundleSource().then(() => {
84 | old.win.reload();
85 | old.win.webContents.reloadIgnoringCache();
86 | old.win.webContents.openDevTools();
87 | console.log("Refreshed!", "#" + Date.now());
88 | });
89 | };
90 | fs.watch("./src/", {recursive: true}, onRefresh);
91 | fs.watch("./static/", {recursive: true}, onRefresh);
92 | });
93 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "poxi",
3 | "version": "0.5.3",
4 | "description": "A modern flat pixel editor",
5 | "license": "BSD-2-Clause",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+ssh://git@github.com/maierfelix/poxi.git"
9 | },
10 | "main": "build.js",
11 | "homepage": "https://github.com/maierfelix/poxi#readme",
12 | "author": "Felix Maier ",
13 | "contributors": [
14 | "Felix Maier (https://github.com/maierfelix)"
15 | ],
16 | "bugs": {
17 | "url": "https://github.com/maierfelix/poxi/issues"
18 | },
19 | "keywords": [],
20 | "scripts": {
21 | "start": "electron ."
22 | },
23 | "engines": {
24 | "node": ">= 6.x"
25 | },
26 | "devDependencies": {
27 | "ws": "^2.3.1",
28 | "electron": "^1.6.6",
29 | "rollup": "^0.41.6",
30 | "rollup-plugin-buble": "^0.15.0"
31 | },
32 | "dependencies": {}
33 | }
34 |
--------------------------------------------------------------------------------
/src/area/functions.js:
--------------------------------------------------------------------------------
1 | import { SELECTION_COLOR } from "../cfg";
2 |
3 | import {
4 | createCanvasBuffer
5 | } from "../utils";
6 |
7 | import {
8 | bytesToRgba,
9 | colorToRgbaString
10 | } from "../color";
11 |
12 | import CommandKind from "../stack/kind";
13 |
14 | /**
15 | * @param {Object} selection
16 | */
17 | export function copy(selection) {
18 | this.clipboard.copy = null;
19 | // shape based selection
20 | if (selection.shape !== null) {
21 | this.copyByShape(selection);
22 | } else {
23 | this.copyBySelection(selection);
24 | }
25 | };
26 |
27 | /**
28 | * Shape-based copying
29 | * @param {Object} selection
30 | */
31 | export function copyByShape(selection) {
32 | const shape = selection.shape;
33 | const data = shape.data;
34 | const bx = shape.bounds.x; const by = shape.bounds.y;
35 | const bw = shape.bounds.w; const bh = shape.bounds.h;
36 | let pixels = [];
37 | for (let ii = 0; ii < data.length; ii += 4) {
38 | const idx = ii / 4;
39 | const xx = idx % bw;
40 | const yy = (idx / bw) | 0;
41 | const px = (yy * bw + xx) * 4;
42 | const alpha = data[px + 3];
43 | // ignore shape pixels that aren't used
44 | if (alpha <= 0) continue;
45 | const pixel = this.getAbsolutePixelAt(bx + xx, by + yy);
46 | if (pixel === null) continue;
47 | pixels.push({
48 | x: xx, y: yy, color: pixel
49 | });
50 | };
51 | this.clipboard.copy = {
52 | pixels: pixels,
53 | selection: selection
54 | };
55 | };
56 |
57 | /**
58 | * Rectangle-based copying
59 | * @param {Object} selection
60 | */
61 | export function copyBySelection(selection) {
62 | const x = selection.x; const y = selection.y;
63 | const w = selection.w; const h = selection.h;
64 | let pixels = [];
65 | for (let ii = 0; ii < w * h; ++ii) {
66 | const xx = ii % w;
67 | const yy = (ii / w) | 0;
68 | const pixel = this.getAbsolutePixelAt(x + xx, y + yy);
69 | if (pixel === null) continue;
70 | pixels.push({
71 | x: xx, y: yy, color: pixel
72 | });
73 | };
74 | this.clipboard.copy = {
75 | pixels: pixels,
76 | selection: selection
77 | };
78 | };
79 |
80 | /**
81 | * @param {Number} x
82 | * @param {Number} y
83 | * @param {Object} sel
84 | * @return {Void}
85 | */
86 | export function pasteAt(x, y, sel) {
87 | const pixels = sel.pixels;
88 | if (pixels === null || !pixels.length) return;
89 | const layer = this.getCurrentLayer();
90 | const batch = layer.createBatchAt(x, y);
91 | batch.resizeRectangular(
92 | x, y,
93 | sel.w - 1, sel.h - 1
94 | );
95 | for (let ii = 0; ii < pixels.length; ++ii) {
96 | const pixel = pixels[ii];
97 | const color = pixel.color;
98 | batch.drawPixelFast(x + pixel.x, y + pixel.y, color);
99 | };
100 | batch.refreshTexture(false);
101 | this.enqueue(CommandKind.PASTE, batch);
102 | return;
103 | };
104 |
105 | /**
106 | * @param {Object} selection
107 | * @return {Void}
108 | */
109 | export function cut(selection) {
110 | this.copy(selection);
111 | const pixels = this.clipboard.copy.pixels;
112 | if (pixels === null || !pixels.length) return;
113 | this.clearSelection(selection);
114 | return;
115 | };
116 |
117 | /**
118 | * Shape-based clearing
119 | * @param {Object} selection
120 | * @return {Void}
121 | */
122 | export function clearSelection(selection) {
123 | const shape = selection.shape;
124 | const bounds = shape.bounds;
125 | const data = shape.data;
126 | const x = selection.x; const y = selection.y;
127 | const w = selection.w; const h = selection.h;
128 | const layer = this.getCurrentLayer();
129 | layer.createBatchAt(x, y);
130 | batch.isEraser = true;
131 | const bw = bounds.w; const bh = bounds.h;
132 | batch.resizeRectangular(
133 | x, y,
134 | w - 1, h - 1
135 | );
136 | let count = 0;
137 | for (let ii = 0; ii < data.length; ii += 4) {
138 | const idx = (ii / 4) | 0;
139 | const xx = (idx % bw) | 0;
140 | const yy = (idx / bw) | 0;
141 | const px = (yy * bw + xx) * 4;
142 | if (data[px + 3] <= 0) continue;
143 | const pixel = this.getAbsolutePixelAt(x + xx, y + yy);
144 | // only erase if we have sth to erase
145 | if (pixel === null) continue;
146 | batch.erasePixelFast(x + xx, y + yy, pixel);
147 | count++;
148 | };
149 | // nothing to change
150 | if (count <= 0) {
151 | batch.kill();
152 | return;
153 | }
154 | batch.refreshTexture(false);
155 | this.enqueue(CommandKind.CLEAR, batch);
156 | return;
157 | };
158 |
159 | /**
160 | * @param {Number} x
161 | * @param {Number} y
162 | * @return {Batch}
163 | */
164 | export function getShapeAt(x, y) {
165 | const color = this.getAbsolutePixelAt(x, y);
166 | if (color === null) return (null);
167 | const shape = this.getBinaryShape(x, y, color);
168 | if (shape === null) return (null);
169 | const batch = this.createDynamicBatch(x, y);
170 | const bounds = this.bounds;
171 | const bx = bounds.x;
172 | const by = bounds.y;
173 | const bw = bounds.w;
174 | const bh = bounds.h;
175 | // create buffer to draw a fake shape into
176 | const buffer = createCanvasBuffer(bw, bh);
177 | const rgba = bytesToRgba(SELECTION_COLOR);
178 | rgba[3] = 0.45;
179 | buffer.fillStyle = colorToRgbaString(rgba);
180 | for (let ii = 0; ii < shape.length; ++ii) {
181 | const xx = (ii % bw);
182 | const yy = (ii / bw) | 0;
183 | if (shape[yy * bw + xx] !== 2) continue;
184 | buffer.fillRect(
185 | xx, yy,
186 | 1, 1
187 | );
188 | };
189 | batch.buffer = buffer;
190 | batch.data = new Uint8Array(buffer.getImageData(0, 0, bw, bh).data);
191 | batch.bounds.update(bx, by, bw, bh);
192 | batch.resizeByMatrixData();
193 | batch.refreshTexture(true);
194 | return (batch);
195 | };
196 |
--------------------------------------------------------------------------------
/src/area/select.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @return {Object}
3 | */
4 | export function getSelection() {
5 | // active shape selection
6 | if (this.shape !== null) {
7 | const bounds = this.shape.bounds;
8 | return ({
9 | shape: this.shape,
10 | x: bounds.x, y: bounds.y,
11 | w: bounds.w, h: bounds.h
12 | });
13 | }
14 | let x = this.sx; let y = this.sy;
15 | let w = this.sw; let h = this.sh;
16 | if (w < 0) x += w;
17 | if (h < 0) y += h;
18 | w = w < 0 ? -w : w;
19 | h = h < 0 ? -h : h;
20 | return ({
21 | shape: null,
22 | x: x, y: y,
23 | w: w, h: h
24 | });
25 | };
26 |
27 | /**
28 | * @param {Number} x
29 | * @param {Number} y
30 | */
31 | export function selectFrom(x, y) {
32 | x = x | 0;
33 | y = y | 0;
34 | const relative = this.getRelativeTileOffset(x, y);
35 | this.sx = relative.x;
36 | this.sy = relative.y;
37 | this.sw = this.sh = 0;
38 | this.redraw = true;
39 | };
40 |
41 | /**
42 | * @param {Number} x
43 | * @param {Number} y
44 | */
45 | export function selectTo(x, y) {
46 | x = x | 0;
47 | y = y | 0;
48 | const relative = this.getRelativeTileOffset(x, y);
49 | let w = relative.x - this.sx;
50 | let h = relative.y - this.sy;
51 | w = w + (w >= 0 ? 1 : 0);
52 | h = h + (h >= 0 ? 1 : 0);
53 | this.sw = w;
54 | this.sh = h;
55 | this.redraw = true;
56 | };
57 |
58 | export function resetSelection() {
59 | this.sx = this.sy = 0;
60 | this.sw = this.sh = -0;
61 | if (this.shape !== null) {
62 | this.destroyTexture(this.shape.texture);
63 | this.shape = null;
64 | }
65 | this.redraw = true;
66 | };
67 |
--------------------------------------------------------------------------------
/src/batch/README.md:
--------------------------------------------------------------------------------
1 | # Batches
2 |
3 | Poxi batches your drawings to allow undo/redo as well as to draw them in an efficient way.
4 |
5 | There are two kinds of batches:
6 | 1. Pixel batches, which contain an Uint8Array storing the drawn pixel data
7 | 2. Erase batches, which contain an Uint8Array storing erased pixel the same way as found in pixel batches, but get handled different in the undo/redo process (inject, deject pixels).
8 |
9 | Batches store a bounding property, which contains their dynamic position on the grid and their absolute width and height. Batch boundings get automatically updated as soon as their content changes. Batches also have a method to resize down their pixel data by the minimum boundings, e.g. auto cropping out unused parts of their content.
10 |
11 | Batches also contain a WebGLTexture property, which is the reference to the active texture on the GPU. As soon as a batch's pixel data get's changed, it's texture gets automatically updated and/or destroyed if it got resized as well. When a batch gets destroyed (e.g. by undoing or exceeding the undo stack size limit), the texture also gets automatically freed from the GPU's memory.
12 |
--------------------------------------------------------------------------------
/src/batch/filter/blend.js:
--------------------------------------------------------------------------------
1 | import { SETTINGS } from "../../cfg";
2 |
3 | import CommandKind from "../../stack/kind";
4 |
5 | /**
6 | * Shade or tint
7 | * @param {Number} x
8 | * @param {Number} y
9 | * @param {Number} factor
10 | */
11 | export function applyColorLightness(x, y, factor) {
12 | const instance = this.instance;
13 | const bounds = this.bounds;
14 | const layer = this.layer;
15 | const w = SETTINGS.LIGHT_SIZE;
16 | const h = SETTINGS.LIGHT_SIZE;
17 | for (let ii = 0; ii < w * h; ++ii) {
18 | const xx = x + (ii % w) | 0;
19 | const yy = y + (ii / w) | 0;
20 | const pixel = layer.getLivePixelAt(xx, yy);
21 | if (pixel === null) continue;
22 | const t = factor < 0 ? 0 : 255;
23 | const p = factor < 0 ? -factor : factor;
24 | const r = (Math.round((t - pixel[0]) * p) + pixel[0]);
25 | const g = (Math.round((t - pixel[1]) * p) + pixel[1]);
26 | const b = (Math.round((t - pixel[2]) * p) + pixel[2]);
27 | const a = pixel[3];
28 | this.drawPixel(xx, yy, [r, g, b, a]);
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/batch/filter/invert.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maierfelix/poxi/08e58c0216e7fda7c975321c1ca75407929f4063/src/batch/filter/invert.js
--------------------------------------------------------------------------------
/src/batch/filter/onion.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maierfelix/poxi/08e58c0216e7fda7c975321c1ca75407929f4063/src/batch/filter/onion.js
--------------------------------------------------------------------------------
/src/batch/filter/replace.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maierfelix/poxi/08e58c0216e7fda7c975321c1ca75407929f4063/src/batch/filter/replace.js
--------------------------------------------------------------------------------
/src/batch/filter/shading.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maierfelix/poxi/08e58c0216e7fda7c975321c1ca75407929f4063/src/batch/filter/shading.js
--------------------------------------------------------------------------------
/src/batch/filter/smoothing.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Remove L shaped corners
4 | * http://deepnight.net/pixel-perfect-drawing/
5 | * @param {Batch} batch
6 | */
7 | export function applyPixelSmoothing(batch) {
8 | const bw = batch.bounds.w;
9 | const bh = batch.bounds.h;
10 | const tiles = batch.data;
11 | for (let ii = 0; ii < tiles.length; ii += 4) {
12 | if (!(ii > 0 && ii + 1 < tiles.length)) continue;
13 | const x = (ii % bw);
14 | const y = (ii / bw) | 0;
15 | const px = (yy * bw + xx) * 4;
16 | if (
17 | (w.x === o.x || w.y === o.y) &&
18 | (e.x === o.x || e.y === o.y) &&
19 | (w.x !== e.x) && (w.y !== e.y)
20 | ) {
21 | tiles[ii + 0] = 0;
22 | tiles[ii + 1] = 0;
23 | tiles[ii + 2] = 0;
24 | tiles[ii + 3] = 0;
25 | }
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/src/batch/index.js:
--------------------------------------------------------------------------------
1 | import { MAX_SAFE_INTEGER } from "../cfg";
2 |
3 | import {
4 | uid,
5 | createCanvasBuffer
6 | } from "../utils";
7 |
8 | import { colorToRgbaString } from "../color";
9 |
10 | import extend from "../extend";
11 |
12 | import Boundings from "../bounds/index";
13 |
14 | import * as _tile from "./tile";
15 | import * as _matrix from "./matrix";
16 |
17 | import * as _blend from "./filter/blend";
18 | import * as _invert from "./filter/invert";
19 | import * as _onion from "./filter/onion";
20 | import * as _replace from "./filter/replace";
21 | import * as _shading from "./filter/shading";
22 | import * as _smoothing from "./filter/smoothing";
23 |
24 | /**
25 | * @class Batch
26 | */
27 | class Batch {
28 | /**
29 | * @param {Poxi} instance
30 | * @constructor
31 | */
32 | constructor(instance) {
33 | this.id = uid();
34 | // save reference to layer
35 | this.layer = null;
36 | // reference to stage
37 | this.instance = instance;
38 | // data related
39 | this.data = null;
40 | this.reverse = null;
41 | // buffer related
42 | // used for canvas batches
43 | this.buffer = null;
44 | // wgl texture
45 | // TODO: textures can possibly get from gpu as soon as mouseUp event passed
46 | this.texture = null;
47 | // relative boundings
48 | this.bounds = new Boundings();
49 | // we use this batch for erasing
50 | this.isEraser = false;
51 | // we use this batch for moving
52 | this.isMover = false;
53 | this.position = { x: 0, y: 0, mx: 0, my: 0 };
54 | // indicates if we should force to render this batch
55 | // even when it is not registered inside our stack yet
56 | this.forceRendering = false;
57 | }
58 | };
59 |
60 | /**
61 | * @return {Batch}
62 | */
63 | Batch.prototype.clone = function() {
64 | const batch = new Batch(this.instance);
65 | batch.prepareMatrix(this.bounds.x, this.bounds.y);
66 | // clone props
67 | batch.layer = this.layer;
68 | batch.buffer = this.buffer;
69 | batch.bounds = this.bounds.clone();
70 | batch.isEraser = this.isEraser;
71 | batch.isMover = this.isMover;
72 | batch.position = Object.assign(this.position);
73 | batch.forceRendering = this.forceRendering;
74 | // clone matrices
75 | batch.data = new Uint8Array(this.data);
76 | batch.reverse = new Uint8Array(this.reverse);
77 | batch.refreshTexture(true);
78 | return (batch);
79 | };
80 |
81 | /**
82 | * @param {Number} x
83 | * @param {Number} y
84 | */
85 | Batch.prototype.move = function(x, y) {
86 | const xx = x - this.position.mx;
87 | const yy = y - this.position.my;
88 | this.position.x += xx;
89 | this.position.y += yy;
90 | this.layer.x += xx;
91 | this.layer.y += yy;
92 | this.position.mx = x;
93 | this.position.my = y;
94 | };
95 |
96 | /**
97 | * @return {Number}
98 | */
99 | Batch.prototype.getStackIndex = function() {
100 | const id = this.id;
101 | const commands = this.instance.stack;
102 | for (let ii = 0; ii < commands.length; ++ii) {
103 | const cmd = commands[ii];
104 | if (cmd.batch.id === id) return (ii);
105 | };
106 | return (-1);
107 | };
108 |
109 | Batch.prototype.kill = function() {
110 | const id = this.id;
111 | const instance = this.instance;
112 | const layers = instance.layers;
113 | // free batch from memory
114 | this.bounds = null;
115 | this.buffer = null;
116 | this.data = null;
117 | this.reverse = null;
118 | this.destroyTexture();
119 | // finally remove batch from layer
120 | let count = 0;
121 | for (let ii = 0; ii < layers.length; ++ii) {
122 | const batches = layers[ii].batches;
123 | for (let jj = 0; jj < batches.length; ++jj) {
124 | const batch = batches[jj];
125 | if (batch.id === id) {
126 | batches.splice(jj, 1);
127 | layers[ii].updateBoundings();
128 | count++;
129 | }
130 | };
131 | };
132 | /*if (count <= 0 && !this.isMover) {
133 | throw new Error(`Failed to kill batch:${this.id}`);
134 | }*/
135 | };
136 |
137 | /**
138 | * Frees the batch texture from the gpu
139 | */
140 | Batch.prototype.destroyTexture = function() {
141 | const texture = this.texture;
142 | if (texture !== null) {
143 | this.instance.destroyTexture(texture);
144 | this.texture = null;
145 | }
146 | };
147 |
148 | /**
149 | * @param {Boolean} state
150 | */
151 | Batch.prototype.refreshTexture = function(resized) {
152 | const bounds = this.bounds;
153 | const bw = bounds.w; const bh = bounds.h;
154 | const instance = this.instance;
155 | if (resized) {
156 | // free old texture from memory
157 | if (this.texture !== null) {
158 | instance.destroyTexture(this.texture);
159 | }
160 | this.texture = instance.bufferTexture(this.id, this.data, bw, bh);
161 | } else {
162 | instance.updateTexture(this.texture, this.data, bw, bh);
163 | }
164 | // trigger our stage to get redrawn
165 | this.instance.redraw = true;
166 | };
167 |
168 | extend(Batch, _tile);
169 | extend(Batch, _matrix);
170 |
171 | extend(Batch, _blend);
172 | extend(Batch, _invert);
173 | extend(Batch, _onion);
174 | extend(Batch, _replace);
175 | extend(Batch, _shading);
176 | extend(Batch, _smoothing);
177 |
178 | export default Batch;
179 |
--------------------------------------------------------------------------------
/src/batch/matrix.js:
--------------------------------------------------------------------------------
1 | import {
2 | MAX_SAFE_INTEGER,
3 | BATCH_JUMP_RESIZE
4 | } from "../cfg";
5 |
6 | import {
7 | alphaByteToRgbAlpha,
8 | additiveAlphaColorBlending
9 | } from "../color";
10 |
11 | /**
12 | * @param {Number} x
13 | * @param {Number} y
14 | * @param {Number} w
15 | * @param {Number} h
16 | */
17 | export function resizeRectangular(x, y, w, h) {
18 | this.resizeByOffset(x, y);
19 | this.resizeByOffset(x + w, y + h);
20 | };
21 |
22 | /**
23 | * @param {Number} x
24 | * @param {Number} y
25 | */
26 | export function resizeByOffset(x, y) {
27 | const bounds = this.bounds;
28 | let bx = bounds.x; let by = bounds.y;
29 | const dx = x - this.layer.x;
30 | const dy = y - this.layer.y;
31 | const w = (Math.abs(bx - dx) | 0) + 1;
32 | const h = (Math.abs(by - dy) | 0) + 1;
33 | const ox = bx; const oy = by;
34 | const ow = bounds.w; const oh = bounds.h;
35 | const xx = -(bx - dx) | 0;
36 | const yy = -(by - dy) | 0;
37 | // resize bound rect to left, top
38 | if (xx < 0) {
39 | bx += xx - BATCH_JUMP_RESIZE;
40 | bounds.w += Math.abs(xx) + BATCH_JUMP_RESIZE;
41 | }
42 | if (yy < 0) {
43 | by += yy - BATCH_JUMP_RESIZE;
44 | bounds.h += Math.abs(yy) + BATCH_JUMP_RESIZE;
45 | }
46 | // resize bound to right, bottom
47 | if (w > bounds.w) bounds.w = w + BATCH_JUMP_RESIZE;
48 | if (h > bounds.h) bounds.h = h + BATCH_JUMP_RESIZE;
49 | bounds.x = bx; bounds.y = by;
50 | // make sure we only resize if necessary
51 | if (ow !== bounds.w || oh !== bounds.h) {
52 | this.resizeMatrix(
53 | ox - bx, oy - by,
54 | bounds.w - ow, bounds.h - oh
55 | );
56 | }
57 | };
58 |
59 | /**
60 | * Resizes matrix by calculating min/max of x,y,w,h
61 | * @return {Void}
62 | */
63 | export function resizeByMatrixData() {
64 | const data = this.data;
65 | const bounds = this.bounds;
66 | const bx = bounds.x; const by = bounds.y;
67 | const bw = bounds.w; const bh = bounds.h;
68 | const ox = bounds.x; const oy = bounds.y;
69 | const ow = bounds.w; const oh = bounds.h;
70 | let x = MAX_SAFE_INTEGER; let y = MAX_SAFE_INTEGER;
71 | let w = -MAX_SAFE_INTEGER; let h = -MAX_SAFE_INTEGER;
72 | let count = 0;
73 | for (let ii = 0; ii < data.length; ii += 4) {
74 | const idx = (ii / 4) | 0;
75 | const xx = (idx % bw) | 0;
76 | const yy = (idx / bw) | 0;
77 | const px = 4 * (yy * bw + xx);
78 | const r = data[px + 0];
79 | const g = data[px + 1];
80 | const b = data[px + 2];
81 | const a = data[px + 3];
82 | // ignore empty tiles
83 | if (a <= 0) continue;
84 | // x, y
85 | if (xx >= 0 && xx <= x) x = xx;
86 | if (yy >= 0 && yy <= y) y = yy;
87 | // width, height
88 | if (xx >= 0 && xx >= w) w = xx;
89 | if (yy >= 0 && yy >= h) h = yy;
90 | count++;
91 | };
92 | const nx = (w - (-x + w));
93 | const ny = (h - (-y + h));
94 | const nbx = bounds.x + nx; const nby = bounds.y + ny;
95 | const nbw = (-x + w) + 1; const nbh = (-y + h) + 1;
96 | // abort if nothing has changed
97 | if (count <= 0) return;
98 | if (ox === nbx && oy === nby && ow === nbw && oh === nbh) return;
99 | bounds.x = nbx; bounds.y = nby;
100 | bounds.w = nbw; bounds.h = nbh;
101 | this.resizeMatrix(
102 | ox - nbx, oy - nby,
103 | nbw - ow, nbh - oh
104 | );
105 | return;
106 | };
107 |
108 | /**
109 | * Resize internal array buffers
110 | * and join old matrix with new one
111 | * @param {Number} x - Resize left
112 | * @param {Number} y - Resize top
113 | * @param {Number} w - Resize right
114 | * @param {Number} h - Resize bottom
115 | * @return {Void}
116 | */
117 | export function resizeMatrix(x, y, w, h) {
118 | const data = this.data;
119 | const rdata = this.reverse;
120 | const nw = this.bounds.w; const nh = this.bounds.h;
121 | const ow = nw - w; const oh = nh - h;
122 | const size = 4 * (nw * nh);
123 | const buffer = new Uint8Array(size);
124 | const reverse = new Uint8Array(size);
125 | for (let ii = 0; ii < data.length; ii += 4) {
126 | const idx = (ii / 4) | 0;
127 | const xx = (idx % ow) | 0;
128 | const yy = (idx / ow) | 0;
129 | const opx = 4 * (yy * ow + xx);
130 | // black magic 🦄
131 | const npx = opx + (yy * (nw - ow) * 4) + (x * 4) + ((y * 4) * nw);
132 | if (data[opx + 3] <= 0) continue;
133 | // refill data
134 | buffer[npx + 0] = data[opx + 0];
135 | buffer[npx + 1] = data[opx + 1];
136 | buffer[npx + 2] = data[opx + 2];
137 | buffer[npx + 3] = data[opx + 3];
138 | if (rdata[opx + 3] <= 0) continue;
139 | // refill reverse data
140 | reverse[npx + 0] = rdata[opx + 0];
141 | reverse[npx + 1] = rdata[opx + 1];
142 | reverse[npx + 2] = rdata[opx + 2];
143 | reverse[npx + 3] = rdata[opx + 3];
144 | };
145 | this.data = buffer;
146 | this.reverse = reverse;
147 | this.refreshTexture(true);
148 | return;
149 | };
150 |
151 | /**
152 | * Merges given matrix
153 | * @param {Batch} batch
154 | * @param {Number} px - X-offset to merge at
155 | * @param {Number} py - Y-offset to merge at
156 | * @param {Boolean} state - Add or reverse
157 | */
158 | export function injectMatrix(batch, state) {
159 | const buffer = this.data;
160 | const isEraser = batch.isEraser;
161 | const data = state ? batch.data : batch.reverse;
162 | const bw = batch.bounds.w | 0;
163 | const bx = (this.bounds.x) | 0;
164 | const by = (this.bounds.y) | 0;
165 | const dx = (batch.bounds.x - bx) | 0;
166 | const dy = (batch.bounds.y - by) | 0;
167 | const w = this.bounds.w | 0; const h = this.bounds.h | 0;
168 | const x = bx | 0; const y = by | 0;
169 | // loop given batch data and merge it with our main matrix
170 | for (let ii = 0; ii < data.length; ii += 4) {
171 | const idx = (ii / 4) | 0;
172 | const xx = (idx % bw) | 0;
173 | const yy = (idx / bw) | 0;
174 | const opx = ((yy * bw + xx) * 4) | 0;
175 | const npx = (opx + (yy * (w - bw) * 4) + (dx * 4) + ((dy * 4) * w)) | 0;
176 | const alpha = data[opx + 3] | 0;
177 | // erase pixel
178 | if (isEraser === true) {
179 | if (state === true && alpha > 0) {
180 | buffer[npx + 0] = buffer[npx + 1] = buffer[npx + 2] = buffer[npx + 3] = 0;
181 | continue;
182 | }
183 | }
184 | // ignore empty data pixels
185 | if (alpha <= 0 && state === true) continue;
186 | // only overwrite the reverse batch's used pixels
187 | if (state === false && alpha <= 0 && batch.data[opx + 3] <= 0) continue;
188 | // manual color blending
189 | if (buffer[npx + 3] > 0 && alpha < 255 && alpha > 0) {
190 | const src = buffer.subarray(npx, npx + 4);
191 | const dst = data.subarray(opx, opx + 4);
192 | // manual color blending
193 | if (state) {
194 | const src = buffer.subarray(npx, npx + 4);
195 | const dst = data.subarray(opx, opx + 4);
196 | const color = additiveAlphaColorBlending(src, dst);
197 | buffer[npx + 0] = color[0];
198 | buffer[npx + 1] = color[1];
199 | buffer[npx + 2] = color[2];
200 | buffer[npx + 3] = color[3];
201 | continue;
202 | // simply fill with reverse data
203 | } else {
204 | buffer[npx + 0] = data[opx + 0];
205 | buffer[npx + 1] = data[opx + 1];
206 | buffer[npx + 2] = data[opx + 2];
207 | buffer[npx + 3] = data[opx + 3];
208 | continue;
209 | }
210 | }
211 | // just fill colors with given batch data kind
212 | buffer[npx + 0] = data[opx + 0];
213 | buffer[npx + 1] = data[opx + 1];
214 | buffer[npx + 2] = data[opx + 2];
215 | buffer[npx + 3] = data[opx + 3];
216 | };
217 | };
218 |
219 | /**
220 | * @param {Number} x
221 | * @param {Number} y
222 | * @return {Array}
223 | */
224 | export function getRawPixelAt(x, y) {
225 | // normalize coordinates
226 | const xx = x - (this.bounds.x);
227 | const yy = y - (this.bounds.y);
228 | // now extract the data
229 | const data = this.data;
230 | // imagedata array is 1d
231 | const idx = 4 * (yy * this.bounds.w + xx);
232 | // pixel index out of bounds
233 | if (idx < 0 || idx >= data.length) return (null);
234 | // get each color value
235 | const r = data[idx + 0];
236 | const g = data[idx + 1];
237 | const b = data[idx + 2];
238 | const a = data[idx + 3];
239 | // dont return anything if we got no valid color
240 | if (a <= 0) return (null);
241 | const color = [r, g, b, alphaByteToRgbAlpha(a)];
242 | // finally return the color array
243 | return (color);
244 | };
245 |
246 | /**
247 | * Clears all pixels non-reversible
248 | */
249 | export function clear() {
250 | const data = this.data;
251 | for (let ii = 0; ii < data.length; ++ii) {
252 | data[ii] = 0;
253 | };
254 | };
255 |
256 | /**
257 | * @param {Number} x
258 | * @param {Number} y
259 | */
260 | export function prepareMatrix(x, y) {
261 | const bounds = this.bounds;
262 | // we don't have a buffer to store data at yet
263 | if (this.data === null) {
264 | bounds.x = x;
265 | bounds.y = y;
266 | bounds.w = 1;
267 | bounds.h = 1;
268 | const size = 4 * (bounds.w * bounds.h);
269 | this.data = new Uint8Array(size);
270 | this.reverse = new Uint8Array(size);
271 | this.texture = this.instance.bufferTexture(this.id, this.data, bounds.w, bounds.h);
272 | }
273 | };
274 |
275 | /**
276 | * Returns the very first found pixel color
277 | * *We expect that the batch is single colored*
278 | * @ignore
279 | * @return {Uint8Array}
280 | */
281 | export function getBatchColor() {
282 | const data = this.data;
283 | const bounds = this.bounds;
284 | const bw = bounds.w; const bh = bounds.h;
285 | // calculate batch color
286 | for (let ii = 0; ii < bw * bh; ++ii) {
287 | const xx = (ii % bw) | 0;
288 | const yy = (ii / bw) | 0;
289 | const px = 4 * (yy * bw + xx);
290 | if (data[px + 3] <= 0) continue;
291 | return (data.subarray(px, px + 4));
292 | };
293 | return (null);
294 | };
295 |
296 | /**
297 | * @return {Boolean}
298 | */
299 | export function isEmpty() {
300 | const data = this.data;
301 | const bw = this.bounds.w;
302 | let count = 0;
303 | for (let ii = 0; ii < data.length; ii += 4) {
304 | const idx = (ii / 4) | 0;
305 | const xx = (idx % bw) | 0;
306 | const yy = (idx / bw) | 0;
307 | const px = (yy * bw + xx) * 4;
308 | const a = data[px + 3];
309 | // ignore empty tiles
310 | if (a <= 0) continue;
311 | count++;
312 | };
313 | return (count <= 0);
314 | };
315 |
--------------------------------------------------------------------------------
/src/batch/tile.js:
--------------------------------------------------------------------------------
1 | import { isPowerOfTwo } from "../math";
2 |
3 | import {
4 | colorToRgbaString,
5 | rgbAlphaToAlphaByte
6 | } from "../color";
7 |
8 | /**
9 | * @param {Number} x
10 | * @param {Number} y
11 | * @param {Number} size
12 | * @param {Array} color
13 | */
14 | export function drawAt(x, y, size, color) {
15 | const xpad = Math.floor(size / 2);
16 | const ypad = Math.floor(size / 2);
17 | this.fillRect(
18 | x - xpad, y - ypad,
19 | size + xpad, size + ypad,
20 | color
21 | );
22 | };
23 |
24 | /**
25 | * @param {Number} x
26 | * @param {Number} y
27 | * @param {Number} w
28 | * @param {Number} h
29 | * @param {Array} color
30 | */
31 | export function fillRect(x, y, w, h, color) {
32 | for (let ii = 0; ii < w * h; ++ii) {
33 | const xx = (ii % w) + x;
34 | const yy = ((ii / w) | 0) + y;
35 | this.drawPixel(xx, yy, color);
36 | };
37 | };
38 |
39 | /**
40 | * @param {Number} x
41 | * @param {Number} y
42 | * @param {Array} color
43 | */
44 | export function drawPixel(x, y, color) {
45 | this.resizeByOffset(x, y);
46 | this.drawPixelFast(x, y, color);
47 | };
48 |
49 | /**
50 | * @param {Number} x
51 | * @param {Number} y
52 | * @param {Array} color
53 | */
54 | export function drawPixelFast(x, y, color) {
55 | const data = this.data;
56 | const rdata = this.reverse;
57 | const dx = x - this.layer.x;
58 | const dy = y - this.layer.y;
59 | const xx = dx - this.bounds.x;
60 | const yy = dy - this.bounds.y;
61 | const idx = 4 * (yy * this.bounds.w + xx);
62 | const pixel = this.layer.getPixelAt(x, y);
63 | // save earlier pixel state into reverse matrix
64 | if (rdata[idx + 3] <= 0 && pixel !== null) {
65 | rdata[idx + 0] = pixel[0];
66 | rdata[idx + 1] = pixel[1];
67 | rdata[idx + 2] = pixel[2];
68 | rdata[idx + 3] = rgbAlphaToAlphaByte(pixel[3]);
69 | }
70 | // overwrite pixel
71 | data[idx + 0] = color[0];
72 | data[idx + 1] = color[1];
73 | data[idx + 2] = color[2];
74 | data[idx + 3] = rgbAlphaToAlphaByte(color[3]);
75 | };
76 |
77 | /**
78 | * @param {Number} x
79 | * @param {Number} y
80 | * @param {Number} size
81 | */
82 | export function clearAt(x, y, size) {
83 | const xpad = Math.floor(size / 2);
84 | const ypad = Math.floor(size / 2);
85 | this.clearRect(
86 | x - xpad, y - ypad,
87 | size + xpad, size + ypad,
88 | color
89 | );
90 | };
91 |
92 | /**
93 | * @param {Number} x
94 | * @param {Number} y
95 | * @param {Number} w
96 | * @param {Number} h
97 | */
98 | export function clearRect(x, y, w, h) {
99 | for (let ii = 0; ii < w * h; ++ii) {
100 | const xx = (ii % w) + x;
101 | const yy = ((ii / w) | 0) + y;
102 | this.erasePixel(xx, yy);
103 | };
104 | };
105 |
106 | /**
107 | * @param {Number} x
108 | * @param {Number} y
109 | * @return {Void}
110 | */
111 | export function erasePixel(x, y) {
112 | const pixel = this.layer.getPixelAt(x, y);
113 | // nothing to erase
114 | if (pixel === null) return;
115 | this.resizeByOffset(x, y);
116 | this.erasePixelFast(x, y, pixel);
117 | return;
118 | };
119 |
120 | /**
121 | * @param {Number} x
122 | * @param {Number} y
123 | * @param {Array} pixel - Earlier pixel
124 | */
125 | export function erasePixelFast(x, y, pixel) {
126 | const data = this.data;
127 | const rdata = this.reverse;
128 | const dx = x - this.layer.x;
129 | const dy = y - this.layer.y;
130 | const xx = dx - this.bounds.x;
131 | const yy = dy - this.bounds.y;
132 | const idx = 4 * (yy * this.bounds.w + xx);
133 | // save old pixel into reverse matrix if not set yet
134 | if (rdata[idx + 3] <= 0) {
135 | rdata[idx + 0] = pixel[0];
136 | rdata[idx + 1] = pixel[1];
137 | rdata[idx + 2] = pixel[2];
138 | rdata[idx + 3] = rgbAlphaToAlphaByte(pixel[3]);
139 | }
140 | // reset pixel data
141 | data[idx + 0] = 255;
142 | data[idx + 1] = 255;
143 | data[idx + 2] = 255;
144 | data[idx + 3] = 255;
145 | };
146 |
--------------------------------------------------------------------------------
/src/bounds/index.js:
--------------------------------------------------------------------------------
1 | import { intersectRectangles } from "../math";
2 |
3 | /**
4 | * @class Boundings
5 | */
6 | class Boundings {
7 | /**
8 | * @param {Number} x
9 | * @param {Number} y
10 | * @param {Number} w
11 | * @param {Number} h
12 | */
13 | constructor(x = 0, y = 0, w = 0, h = 0) {
14 | this.x = 0;
15 | this.y = 0;
16 | this.w = 0;
17 | this.h = 0;
18 | this.update(x, y, w, h);
19 | };
20 | };
21 |
22 | /**
23 | * @return {Boundings}
24 | */
25 | Boundings.prototype.clone = function() {
26 | const bounds = new Boundings(
27 | this.x, this.y, this.w, this.h
28 | );
29 | return (bounds);
30 | };
31 |
32 | /**
33 | * @param {Number} x
34 | * @param {Number} y
35 | * @param {Number} w
36 | * @param {Number} h
37 | */
38 | Boundings.prototype.update = function(x, y, w, h) {
39 | x = x | 0;
40 | y = y | 0;
41 | w = w | 0;
42 | h = h | 0;
43 | this.x = x;
44 | this.y = y;
45 | this.w = w;
46 | this.h = h;
47 | };
48 |
49 | /**
50 | * @param {Boundings} bounds
51 | */
52 | Boundings.prototype.updateByBoundings = function(bounds) {
53 | this.x = bounds.x | 0;
54 | this.y = bounds.y | 0;
55 | this.w = bounds.w | 0;
56 | this.h = bounds.h | 0;
57 | };
58 |
59 | /**
60 | * @param {Number} x
61 | * @param {Number} y
62 | * @return {Boolean}
63 | */
64 | Boundings.prototype.isPointInside = function(x, y) {
65 | x = x | 0;
66 | y = y | 0;
67 | const state = intersectRectangles(
68 | this.x, this.y, this.w - 1, this.h - 1,
69 | x, y, 0, 0
70 | );
71 | return (state);
72 | };
73 |
74 | export default Boundings;
75 |
--------------------------------------------------------------------------------
/src/camera/functions.js:
--------------------------------------------------------------------------------
1 | import {
2 | TILE_SIZE,
3 | MIN_SCALE,
4 | MAX_SCALE,
5 | ZOOM_SPEED,
6 | MAGIC_SCALE
7 | } from "../cfg";
8 |
9 | import {
10 | roundTo,
11 | zoomScale,
12 | alignToGrid
13 | } from "../math";
14 |
15 | /**
16 | * @param {Number} dir
17 | */
18 | export function scale(dir) {
19 | const x = (dir * (ZOOM_SPEED / 1e2)) * zoomScale(this.cs);
20 | const oscale = this.cs;
21 | if (this.cs + x <= MIN_SCALE) this.cs = MIN_SCALE;
22 | else if (this.cs + x >= MAX_SCALE) this.cs = MAX_SCALE;
23 | else this.cs += x;
24 | this.cs = roundTo(this.cs, MAGIC_SCALE);
25 | if (this.cs >= (MAX_SCALE - 1) + .25) this.cs = (MAX_SCALE - 1) + .25;
26 | this.cx -= (this.lx) * (zoomScale(this.cs) - zoomScale(oscale));
27 | this.cy -= (this.ly) * (zoomScale(this.cs) - zoomScale(oscale));
28 | this.cr = roundTo(this.cs, MAGIC_SCALE);
29 | this.updateGrid();
30 | };
31 |
32 | /**
33 | * @param {Number} x
34 | * @param {Number} y
35 | */
36 | export function hover(x, y) {
37 | x = x | 0;
38 | y = y | 0;
39 | this.mx = x;
40 | this.my = y;
41 | };
42 |
43 | /**
44 | * @param {Number} x
45 | * @param {Number} y
46 | */
47 | export function click(x, y) {
48 | x = x | 0;
49 | y = y | 0;
50 | const position = this.getRelativeOffset(x, y);
51 | this.dx = x;
52 | this.dy = y;
53 | this.lx = position.x;
54 | this.ly = position.y;
55 | };
56 |
57 | /**
58 | * @param {Number} x
59 | * @param {Number} y
60 | */
61 | export function drag(x, y) {
62 | x = x | 0;
63 | y = y | 0;
64 | this.cx += x - this.dx;
65 | this.cy += y - this.dy;
66 | this.dx = x;
67 | this.dy = y;
68 | this.updateGrid();
69 | };
70 |
71 | /**
72 | * @param {Number} x
73 | * @param {Number} y
74 | * @return {Object}
75 | */
76 | export function getRelativeOffset(x, y) {
77 | x = x | 0;
78 | y = y | 0;
79 | const xx = (x - this.cx) / this.cs;
80 | const yy = (y - this.cy) / this.cs;
81 | return ({
82 | x: xx,
83 | y: yy
84 | });
85 | };
86 |
87 | /**
88 | * @param {Number} x
89 | * @param {Number} y
90 | * @return {Object}
91 | */
92 | export function getRelativeTileOffset(x, y) {
93 | x = x | 0;
94 | y = y | 0;
95 | const rel = this.getRelativeOffset(x, y);
96 | return (
97 | this.getTileOffsetAt(rel.x, rel.y)
98 | );
99 | };
100 |
101 | /**
102 | * @param {Number} x
103 | * @param {Number} y
104 | * @return {Object}
105 | */
106 | export function getTileOffsetAt(x, y) {
107 | x = x | 0;
108 | y = y | 0;
109 | const half = TILE_SIZE / 2;
110 | const xx = alignToGrid(x - half);
111 | const yy = alignToGrid(y - half);
112 | return ({
113 | x: (xx / TILE_SIZE) | 0,
114 | y: (yy / TILE_SIZE) | 0
115 | });
116 | };
117 |
118 | /**
119 | * @param {Boundings} bounds
120 | */
121 | export function boundsInsideView(bounds) {
122 | const cs = this.cs;
123 | const ww = (bounds.w * TILE_SIZE) * cs;
124 | const hh = (bounds.h * TILE_SIZE) * cs;
125 | const xx = ((bounds.x * TILE_SIZE) * cs) + this.cx;
126 | const yy = ((bounds.y * TILE_SIZE) * cs) + this.cy;
127 | return (
128 | (xx + ww >= 0 && xx <= this.cw) &&
129 | (yy + hh >= 0 && yy <= this.ch)
130 | );
131 | };
132 |
--------------------------------------------------------------------------------
/src/cfg.js:
--------------------------------------------------------------------------------
1 | // default view size
2 | export const DEFAULT_WIDTH = 480;
3 | export const DEFAULT_HEIGHT = 320;
4 | // default grid hidden or not
5 | export const DEFAULT_GRID_HIDDEN = false;
6 |
7 | export const TILE_SIZE = 8;
8 | export const MIN_SCALE = 0.1;
9 | export const MAX_SCALE = 32;
10 | export const BASE_SCALE = 1;
11 | export const MAGIC_SCALE = .125;
12 | // trace ghost tiles by alpha=^2
13 | export const UNSET_TILE_COLOR = 2;
14 | export const ERASE_TILE_COLOR = [0, 1, 0, 0.1];
15 | export const BASE_TILE_COLOR = [0, 0, 0, 0];
16 | export const SELECTION_COLOR = [1, 1, 1, 0.1];
17 | export const SELECTION_COLOR_ACTIVE = [1, 1, 1, 0.2];
18 | export const TILE_HOVER_COLOR = [1, 1, 1, 0.2];
19 |
20 | // 32-bit ints are allowed at maximum
21 | export const MAX_SAFE_INTEGER = (2 ** 31) - 1;
22 |
23 | // alpha byte to rgb-alpha conversion
24 | export const MAGIC_RGB_A_BYTE = 0.00392;
25 |
26 | // factor when to hide the grid
27 | export const HIDE_GRID = 1.0;
28 | export const GRID_LINE_WIDTH = 0.25;
29 |
30 | // how fast we can scale with our mouse wheel
31 | export const ZOOM_SPEED = 15;
32 |
33 | // base step size we jump when resizing batches
34 | export const BATCH_JUMP_RESIZE = 64;
35 |
36 | // Maximum allowed items inside stack
37 | export const STACK_LIMIT = 128;
38 |
39 | // WebGL texture limit
40 | export const WGL_TEXTURE_LIMIT = STACK_LIMIT * 2;
41 |
42 | // WebGL supported or not
43 | export const WGL_SUPPORTED = (
44 | typeof WebGLRenderingContext !== "undefined"
45 | );
46 |
47 | // WebAssembly supported or not
48 | export const WASM_SUPPORTED = (
49 | typeof WebAssembly !== "undefined"
50 | );
51 |
52 | // dev mode state
53 | export let MODES = {
54 | DEV: false
55 | };
56 |
57 | // different settings
58 | export let SETTINGS = {
59 | LIGHT_SIZE: 1,
60 | PENCIL_SIZE: 1,
61 | ERASER_SIZE: 1,
62 | LIGHTING_MODE: 0.05
63 | };
64 |
65 | export const STORAGE_KEY = "poxi";
66 | export const STORAGE_OBJECT = window.localStorage;
67 |
68 | // asset path
69 | export const ASSET_PATH = "./assets/";
70 |
71 | // light bulb icon res
72 | export const LIGHT_DARKEN_IMG_PATH = ASSET_PATH + "img/light_off.png";
73 | export const LIGHT_LIGHTEN_IMG_PATH = ASSET_PATH + "img/light_on.png";
74 |
--------------------------------------------------------------------------------
/src/color.js:
--------------------------------------------------------------------------------
1 | import { MAGIC_RGB_A_BYTE } from "./cfg";
2 |
3 | /**
4 | * 0-255 => 0-1 with precision 1
5 | * @param {Number} a
6 | * @return {Number}
7 | */
8 | export function alphaByteToRgbAlpha(a) {
9 | return ((a * MAGIC_RGB_A_BYTE) * 10) / 10;
10 | };
11 |
12 | /**
13 | * 0-1 => 0-255
14 | * Derivative of alphaByteToRgbAlpha
15 | * @param {Number} a
16 | * @return {Number}
17 | */
18 | export function rgbAlphaToAlphaByte(a) {
19 | return ((a / MAGIC_RGB_A_BYTE) * 10) / 10;
20 | };
21 |
22 | /**
23 | * Convert rgba to rgba byte color
24 | * @param {Array} rgba
25 | * @return {Array}
26 | */
27 | export function rgbaToBytes(rgba) {
28 | const r = rgba[0] / 255;
29 | const g = rgba[1] / 255;
30 | const b = rgba[2] / 255;
31 | const a = rgba[3];
32 | return ([r, g, b, a]);
33 | };
34 |
35 | /**
36 | * Convert bytes to rgba color
37 | * @param {Array} bytes
38 | * @return {Array}
39 | */
40 | export function bytesToRgba(bytes) {
41 | const r = bytes[0] * 255;
42 | const g = bytes[1] * 255;
43 | const b = bytes[2] * 255;
44 | const a = bytes[3];
45 | return ([r, g, b, a]);
46 | };
47 |
48 | /**
49 | * @param {Uint8Array} aa
50 | * @param {Uint8Array} bb
51 | * @param {Number} dir
52 | * @return {Uint8Array}
53 | */
54 | export function rgbaDifference(aa, bb, dir) {
55 | const rgba = new Uint8Array(4);
56 | rgba[0] = aa[0] + bb[0] * dir;
57 | rgba[1] = aa[1] + bb[1] * dir;
58 | rgba[2] = aa[2] + bb[2] * dir;
59 | rgba[3] = aa[3] + bb[3] * dir;
60 | return (rgba);
61 | };
62 |
63 | /**
64 | * Additive color blending with alpha support
65 | * @param {Uint8Array} src
66 | * @param {Uint8Array} dst
67 | * @return {Uint8Array}
68 | */
69 | export function additiveAlphaColorBlending(src, dst) {
70 | const a1 = ((src[3] * MAGIC_RGB_A_BYTE) * 10) / 10;
71 | const a2 = ((dst[3] * MAGIC_RGB_A_BYTE) * 10) / 10;
72 | const a = 1 - (1 - a2) * (1 - a1);
73 | src[0] = ((dst[0] * a2 / a) + (src[0] * a1 * (1 - a2) / a)) | 0;
74 | src[1] = ((dst[1] * a2 / a) + (src[1] * a1 * (1 - a2) / a)) | 0;
75 | src[2] = ((dst[2] * a2 / a) + (src[2] * a1 * (1 - a2) / a)) | 0;
76 | src[3] = (((a / MAGIC_RGB_A_BYTE) * 10) / 10) | 0;
77 | return (src);
78 | };
79 |
80 | /**
81 | * @param {Uint8Array} rgba
82 | * @return {Uint8Array}
83 | */
84 | export function invertColors(rgba) {
85 | const data = new Uint8Array(rgba.length);
86 | for (let ii = 0; ii < rgba.length; ii += 4) {
87 | const alpha = rgba[ii + 3];
88 | data[ii + 0] = alpha - rgba[ii + 0];
89 | data[ii + 1] = alpha - rgba[ii + 1];
90 | data[ii + 2] = alpha - rgba[ii + 2];
91 | data[ii + 3] = alpha;
92 | };
93 | return (data);
94 | };
95 |
96 | /**
97 | * @param {Uint8Array} rgba
98 | * @return {Uint8Array}
99 | */
100 | export function grayscaleColors(rgba) {
101 | const data = new Uint8Array(rgba.length);
102 | for (let ii = 0; ii < rgba.length; ii += 4) {
103 | const gr = (
104 | (rgba[ii + 0] * .3) + (rgba[ii + 1] * .59) + (rgba[ii + 2] * .11)
105 | ) | 0;
106 | data[ii + 0] = gr;
107 | data[ii + 1] = gr;
108 | data[ii + 2] = gr;
109 | data[ii + 3] = rgba[ii + 3];
110 | };
111 | return (data);
112 | };
113 |
114 | /**
115 | * @return {Array}
116 | */
117 | export function randomRgbaColor() {
118 | const r = (Math.random() * 256) | 0;
119 | const g = (Math.random() * 256) | 0;
120 | const b = (Math.random() * 256) | 0;
121 | return ([r, g, b, 1]);
122 | };
123 |
124 | let velo = 64;
125 | let rr = 127; let rrr = velo;
126 | let rg = 12; let rrg = velo;
127 | let rb = 108; let rrb = velo;
128 | /**
129 | * @return {Array}
130 | */
131 | export function getRainbowColor() {
132 | rr += rrr;
133 | if (rr >= 255) rrr = -velo;
134 | else if (rr <= 0) rrr = velo;
135 | rg += rrg;
136 | if (rg >= 255) rrg = -velo;
137 | else if (rg <= 0) rrg = velo;
138 | rb += rrb;
139 | if (rb >= 255) rrb = -velo;
140 | else if (rb <= 0) rrb = velo;
141 | return ([rr, rg, rb, velo]);
142 | };
143 |
144 | /**
145 | * @param {Array} color
146 | * @return {String}
147 | */
148 | export function colorToRgbaString(color) {
149 | const r = color[0];
150 | const g = color[1];
151 | const b = color[2];
152 | const a = color[3];
153 | return (`rgba(${r},${g},${b},${a})`);
154 | };
155 |
156 | /**
157 | * @param {String} hex
158 | * @return {Array}
159 | */
160 | export function hexToRgba(hex) {
161 | const r = parseInt(hex.substring(1,3), 16);
162 | const g = parseInt(hex.substring(3,5), 16);
163 | const b = parseInt(hex.substring(5,7), 16);
164 | return ([r, g, b, 1]);
165 | };
166 |
167 | /**
168 | * @param {Array} rgba
169 | * @return {String}
170 | */
171 | export function rgbaToHex(rgba) {
172 | const r = rgba[0];
173 | const g = rgba[1];
174 | const b = rgba[2];
175 | const a = rgba[3];
176 | return (
177 | "#" +
178 | ("0" + parseInt(r, 10).toString(16)).slice(-2) +
179 | ("0" + parseInt(g, 10).toString(16)).slice(-2) +
180 | ("0" + parseInt(b, 10).toString(16)).slice(-2)
181 | );
182 | };
183 |
184 | /**
185 | * Do rgba color arrays match
186 | * @param {Array} a
187 | * @param {Array} a
188 | * @return {Boolean}
189 | */
190 | export function colorsMatch(a, b) {
191 | return (
192 | a[0] === b[0] &&
193 | a[1] === b[1] &&
194 | a[2] === b[2] &&
195 | a[3] === b[3]
196 | );
197 | };
198 |
199 | /**
200 | * Checks if a color array is fully transparent
201 | * @param {Array} color
202 | * @return {Boolean}
203 | */
204 | const transparent = [0, 0, 0, 0];
205 | export function isGhostColor(color) {
206 | return (colorsMatch(color, transparent));
207 | };
208 |
--------------------------------------------------------------------------------
/src/container/index.js:
--------------------------------------------------------------------------------
1 | import { TILE_SIZE } from "../cfg";
2 | import {
3 | uid,
4 | createCanvasBuffer
5 | } from "../utils";
6 |
7 | import { intersectRectangles } from "../math";
8 |
9 | import {
10 | colorToRgbaString,
11 | rgbAlphaToAlphaByte
12 | } from "../color";
13 |
14 | import extend from "../extend";
15 | import Layer from "../layer/index";
16 | import Boundings from "../bounds/index";
17 |
18 | /**
19 | * @class Container
20 | * @extends Layer
21 | */
22 | class Container extends Layer {
23 | /**
24 | * @param {Poxi} instance
25 | * @constructor
26 | */
27 | constructor(instance) {
28 | super(instance);
29 | this.animation = {
30 | w: 32, h: 32,
31 | frame: 0, speed: 0
32 | };
33 | // tile amount x
34 | this.bounds.w = 3;
35 | // tile amount y
36 | this.bounds.h = 4;
37 | }
38 | };
39 |
40 | /**
41 | * @param {Number} index
42 | * @return {CanvasRenderingContext2D}
43 | */
44 | Container.prototype.getFrameData = function(index) {
45 | const instance = this.instance;
46 | const animation = this.animation;
47 | const fw = animation.w; const fh = animation.h;
48 | const fx = this.bounds.x + (fw * ((index % this.bounds.w) | 0));
49 | const fy = this.bounds.y + (fh * ((index / this.bounds.w) | 0));
50 | const layers = this.instance.layers;
51 | const size = fw * fh;
52 | const buffer = createCanvasBuffer(fw, fh);
53 | for (let ii = 0; ii < size; ++ii) {
54 | const xx = (ii % fw) | 0;
55 | const yy = (ii / fw) | 0;
56 | const pixel = instance.getAbsolutePixelAt(fx + xx, fy + yy);
57 | if (pixel === null) continue;
58 | buffer.fillStyle = colorToRgbaString(pixel);
59 | buffer.fillRect(xx, yy, 1, 1);
60 | };
61 | return (buffer);
62 | };
63 |
64 | /**
65 | * @return {CanvasRenderingContext2D}
66 | */
67 | Container.prototype.getAnimationTemplate = function() {
68 | const instance = this.instance;
69 | const animation = this.animation;
70 | const fw = animation.w; const fh = animation.h;
71 | const fx = this.bounds.x - fw;
72 | const fy = this.bounds.y;
73 | const layers = this.instance.layers;
74 | const size = fw * fh;
75 | const buffer = createCanvasBuffer(fw, fh);
76 | for (let ii = 0; ii < size; ++ii) {
77 | const xx = (ii % fw) | 0;
78 | const yy = (ii / fw) | 0;
79 | const pixel = instance.getAbsolutePixelAt(fx + xx, fy + yy);
80 | if (pixel === null) continue;
81 | buffer.fillStyle = colorToRgbaString(pixel);
82 | buffer.fillRect(xx, yy, 1, 1);
83 | };
84 | return (buffer);
85 | };
86 |
87 | Container.prototype.renderAnimationPreview = function() {
88 | const instance = this.instance;
89 | const cx = instance.cx | 0;
90 | const cy = instance.cy | 0;
91 | const cr = instance.cr;
92 | const buffer = instance.cache.fg;
93 | const animation = this.animation;
94 | const bw = this.bounds.w;
95 | const bh = this.bounds.h;
96 | const tilew = ((animation.w * TILE_SIZE) * cr) | 0;
97 | const tileh = ((animation.h * TILE_SIZE) * cr) | 0;
98 | const ww = (((bw * animation.w) * TILE_SIZE) * cr) | 0;
99 | const hh = (((bh * animation.h) * TILE_SIZE) * cr) | 0;
100 | const xx = (cx + (this.bounds.x * TILE_SIZE) * cr) | 0;
101 | const yy = (cy + (this.bounds.y * TILE_SIZE) * cr) | 0;
102 | const ctx = this.getFrameData(animation.frame);
103 | const canvas = ctx.canvas;
104 | // draw preview border
105 | const preview = this.getAnimationTemplate();
106 | buffer.globalAlpha = 0.25;
107 | const size = ((bw * bh) * animation.w);
108 | for (let ii = 0; ii < size; ii += animation.w) {
109 | const px = (ii % bw) | 0;
110 | const py = ((ii / bw) / animation.w) | 0;
111 | buffer.drawImage(
112 | preview.canvas,
113 | 0, 0,
114 | animation.w, animation.h,
115 | xx + (px * tilew), yy + (py * tileh),
116 | tilew, tileh
117 | );
118 | };
119 | buffer.globalAlpha = 1.0;
120 | };
121 |
122 | Container.prototype.renderFrame = function() {
123 | const instance = this.instance;
124 | const cx = instance.cx | 0;
125 | const cy = instance.cy | 0;
126 | const cr = instance.cr;
127 | const buffer = instance.cache.fg;
128 | const animation = this.animation;
129 | const bw = this.bounds.w;
130 | const bh = this.bounds.h;
131 | const tilew = ((animation.w * TILE_SIZE) * cr) | 0;
132 | const tileh = ((animation.h * TILE_SIZE) * cr) | 0;
133 | const ww = (((bw * animation.w) * TILE_SIZE) * cr) | 0;
134 | const hh = (((bh * animation.h) * TILE_SIZE) * cr) | 0;
135 | const xx = (cx + (this.bounds.x * TILE_SIZE) * cr) | 0;
136 | const yy = (cy + (this.bounds.y * TILE_SIZE) * cr) | 0;
137 | const ax = xx - tilew | 0;
138 | const ay = yy | 0;
139 | const ctx = this.getFrameData(animation.frame);
140 | const canvas = ctx.canvas;
141 | buffer.drawImage(
142 | canvas,
143 | 0, 0,
144 | canvas.width | 0, canvas.height | 0,
145 | ax | 0, ay | 0,
146 | tilew | 0, tileh | 0
147 | );
148 | };
149 |
150 | Container.prototype.renderBoundings = function() {
151 | const instance = this.instance;
152 | const cx = instance.cx | 0;
153 | const cy = instance.cy | 0;
154 | const cr = instance.cr;
155 | const buffer = instance.cache.fg;
156 | const lw = Math.max(0.55, 0.55 * cr);
157 | const animation = this.animation;
158 | const bw = this.bounds.w;
159 | const bh = this.bounds.h;
160 | const tilew = ((animation.w * TILE_SIZE) * cr) | 0;
161 | const tileh = ((animation.h * TILE_SIZE) * cr) | 0;
162 | const ww = (((bw * animation.w) * TILE_SIZE) * cr) | 0;
163 | const hh = (((bh * animation.h) * TILE_SIZE) * cr) | 0;
164 | const xx = (cx + (this.bounds.x * TILE_SIZE) * cr) | 0;
165 | const yy = (cy + (this.bounds.y * TILE_SIZE) * cr) | 0;
166 | const ax = xx - tilew | 0;
167 | const ay = yy | 0;
168 | // draw grid
169 | buffer.strokeStyle = "rgba(255,255,255,0.375)";
170 | buffer.lineWidth = lw;
171 | buffer.beginPath();
172 | for (let ii = tilew; ii < ww; ii += tilew) {
173 | buffer.moveTo(xx + ii, yy);
174 | buffer.lineTo(xx + ii, yy + hh);
175 | };
176 | for (let ii = tileh; ii < hh; ii += tileh) {
177 | buffer.moveTo(xx, yy + ii);
178 | buffer.lineTo(xx + ww, yy + ii);
179 | };
180 | buffer.stroke();
181 | buffer.closePath();
182 | // draw border
183 | instance.drawStrokedRect(xx, yy, ww, hh, "rgba(255,255,255,0.375)");
184 | instance.drawRectangle(ax, ay, tilew - lw, tileh, [1,1,1,0.1]);
185 | instance.drawStrokedRect(ax, ay, tilew - lw, tileh, "rgba(255,255,255,0.375)");
186 | };
187 |
188 | export default Container;
189 |
--------------------------------------------------------------------------------
/src/env/fill.js:
--------------------------------------------------------------------------------
1 | import { BASE_TILE_COLOR } from "../cfg";
2 |
3 | import {
4 | createCanvasBuffer
5 | } from "../utils";
6 |
7 | import {
8 | colorsMatch,
9 | rgbAlphaToAlphaByte
10 | } from "../color";
11 |
12 | import CommandKind from "../stack/kind";
13 |
14 | /**
15 | * Fill enclosed tile area
16 | * @param {Number} x
17 | * @param {Number} y
18 | * @param {Array} color
19 | */
20 | export function fillBucket(x, y, color) {
21 | color = color || [255, 255, 255, 1];
22 | if (color[3] > 1) throw new Error("Invalid alpha color!");
23 | // differentiate between empty and colored tiles
24 | const layer = this.getCurrentLayer();
25 | const bounds = layer.bounds;
26 | const base = layer.getPixelAt(x, y) || BASE_TILE_COLOR;
27 | // clicked tile color and fill colors matches, abort
28 | if (colorsMatch(base, color)) return;
29 | // save the current stack index
30 | const batch = layer.createBatchAt(x, y);
31 | // flood fill
32 | let shape = this.getBinaryShape(x, y, base);
33 | // ups, we filled infinite
34 | if (shape === null) return;
35 | // now fill a buffer by our grid data
36 | const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
37 | const bw = bounds.w; const bh = bounds.h;
38 | const bcolor = [color[0], color[1], color[2], color[3]];
39 | batch.resizeRectangular(
40 | bx, by,
41 | bw, bh
42 | );
43 | let count = 0;
44 | // flood fill pixels
45 | for (let ii = 0; ii < bw * bh; ++ii) {
46 | const xx = (ii % bw) | 0;
47 | const yy = (ii / bw) | 0;
48 | const px = (yy * bw + xx) | 0;
49 | // only fill active grid pixels
50 | if (shape[px] !== 2) continue;
51 | batch.drawPixelFast(bx + xx, by + yy, bcolor);
52 | count++;
53 | };
54 | // nothing changed
55 | if (count <= 0) {
56 | batch.kill();
57 | return;
58 | }
59 | // auto resize batch's size by the used pixel data
60 | batch.resizeByMatrixData();
61 | this.enqueue(CommandKind.FILL, batch);
62 | // free grid from memory
63 | shape = null;
64 | return;
65 | };
66 |
67 | /**
68 | * @param {Number} x
69 | * @param {Number} y
70 | * @return {Void}
71 | */
72 | export function floodPaint(x, y) {
73 | const color = this.fillStyle;
74 | const layer = this.getCurrentLayer();
75 | const bounds = layer.bounds;
76 | const base = layer.getPixelAt(x, y);
77 | // empty base tile or colors to fill are the same
78 | if (base === null || colorsMatch(base, color)) return;
79 | const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
80 | const bw = bounds.w; const bh = bounds.h;
81 | const batch = layer.createBatchAt(bx, by);
82 | batch.resizeRectangular(
83 | bx, by,
84 | bw, bh
85 | );
86 | let count = 0;
87 | // flood paint
88 | for (let ii = 0; ii < bw * bh; ++ii) {
89 | const xx = (ii % bw);
90 | const yy = (ii / bw) | 0;
91 | const pixel = layer.getPixelAt(bx + xx, by + yy);
92 | if (pixel === null) continue;
93 | if (!colorsMatch(base, pixel)) continue;
94 | batch.drawPixelFast(bx + xx, by + yy, color);
95 | count++;
96 | };
97 | // nothing changed
98 | if (count <= 0) {
99 | batch.kill();
100 | return;
101 | }
102 | batch.resizeByMatrixData();
103 | this.enqueue(CommandKind.FLOOD_FILL, batch);
104 | return;
105 | };
106 |
--------------------------------------------------------------------------------
/src/env/functions.js:
--------------------------------------------------------------------------------
1 | import {
2 | SETTINGS,
3 | MAX_SAFE_INTEGER
4 | } from "../cfg";
5 |
6 | import {
7 | colorToRgbaString,
8 | alphaByteToRgbAlpha
9 | } from "../color";
10 |
11 | import Layer from "../layer/index";
12 | import Batch from "../batch/index";
13 | import CommandKind from "../stack/kind";
14 |
15 | /**
16 | * @return {Boolean}
17 | */
18 | export function isInActiveState() {
19 | const states = this.states;
20 | for (let key in states) {
21 | // ignore dragging state
22 | if (key === "dragging") continue;
23 | if (states[key]) return (true);
24 | };
25 | return (false);
26 | };
27 |
28 | /**
29 | * @param {Number} id
30 | * @return {Batch}
31 | */
32 | export function getBatchById(id) {
33 | let result = null;
34 | const layers = this.layers;
35 | for (let ii = 0; ii < layers.length; ++ii) {
36 | const idx = layers.length - 1 - ii;
37 | const layer = layers[idx];
38 | let batch = layer.getBatchById(id);
39 | if (batch !== null) {
40 | result = batch;
41 | break;
42 | }
43 | };
44 | return (result);
45 | };
46 |
47 | /**
48 | * @return {Layer}
49 | */
50 | export function addLayer() {
51 | const layer = new Layer(this);
52 | layer.addUiReference();
53 | this.layers.push(layer);
54 | return (layer);
55 | };
56 |
57 | /**
58 | * @return {Layer}
59 | */
60 | export function getCurrentLayer() {
61 | return (this.activeLayer || null);
62 | };
63 |
64 | /**
65 | * @param {HTMLElement} node
66 | * @return {Layer}
67 | */
68 | export function getLayerByNode(node) {
69 | for (let ii = 0; ii < this.layers.length; ++ii) {
70 | const layer = this.layers[ii];
71 | if (layer.node === node) return (layer);
72 | };
73 | return (null);
74 | };
75 |
76 | /**
77 | * @param {Number} index
78 | * @return {Layer}
79 | */
80 | export function getLayerByIndex(index) {
81 | return (this.layers[index] || null);
82 | };
83 |
84 | /**
85 | * @param {Layer} layer
86 | */
87 | export function setActiveLayer(layer) {
88 | const old = this.getCurrentLayer();
89 | if (old && old.node) {
90 | old.node.classList.remove("selected");
91 | }
92 | if (layer) layer.node.classList.add("selected");
93 | this.activeLayer = layer;
94 | this.redraw = true;
95 | };
96 |
97 | export function refreshUiLayers() {
98 | const layers = this.layers;
99 | for (let ii = 0; ii < layers.length; ++ii) {
100 | const layer = layers[ii];
101 | layer.removeUiReference();
102 | layer.addUiReference();
103 | };
104 | };
105 |
106 | /**
107 | * @param {Number} x
108 | * @param {Number} y
109 | * @return {Layer}
110 | */
111 | export function getLayerByPoint(x, y) {
112 | const layers = this.layers;
113 | // search by active pixel
114 | for (let ii = 0; ii < layers.length; ++ii) {
115 | const layer = layers[ii];
116 | const xx = x - layer.x;
117 | const yy = y - layer.y;
118 | if (layer.locked) continue;
119 | if (layer.bounds.isPointInside(xx, yy)) {
120 | if (layer.getPixelAt(x, y)) {
121 | return (layer);
122 | }
123 | }
124 | };
125 | // active pixel search failed
126 | // so now search by point inside
127 | for (let ii = 0; ii < layers.length; ++ii) {
128 | const idx = layers.length - 1 - ii;
129 | const layer = layers[ii];
130 | const xx = x - layer.x;
131 | const yy = y - layer.y;
132 | if (layer.locked) continue;
133 | if (layer.bounds.isPointInside(xx, yy)) {
134 | return (layer);
135 | }
136 | };
137 | return (null);
138 | };
139 |
140 | /**
141 | * Get batch to insert at by current active state
142 | * @return {Batch}
143 | */
144 | export function getCurrentDrawingBatch() {
145 | for (let key in this.states) {
146 | const state = this.states[key];
147 | if (state === true && this.buffers[key]) {
148 | return (this.buffers[key]);
149 | }
150 | };
151 | return (null);
152 | };
153 |
154 | /**
155 | * @param {Number} x
156 | * @param {Number} y
157 | * @return {Batch}
158 | */
159 | export function createDynamicBatch(x, y) {
160 | const batch = new Batch(this);
161 | batch.prepareMatrix(x, y);
162 | return (batch);
163 | };
164 |
165 | /**
166 | * Get absolute pixel
167 | * @param {Number} x
168 | * @param {Number} y
169 | * @return {Array}
170 | */
171 | export function getAbsolutePixelAt(x, y) {
172 | // normalize coordinates
173 | const bw = this.bounds.w;
174 | const bh = this.bounds.h;
175 | const xx = x - this.bounds.x;
176 | const yy = y - this.bounds.y;
177 | // check if point inside boundings
178 | if (
179 | (xx < 0 || yy < 0) ||
180 | (bw <= 0 || bh <= 0) ||
181 | (xx >= bw || yy >= bh)
182 | ) return (null);
183 | // go through each layer reversed
184 | // and search for the given pixel
185 | const layers = this.layers;
186 | for (let ii = 0; ii < layers.length; ++ii) {
187 | const layer = layers[ii];
188 | if (!layer.visible) continue;
189 | const pixel = layer.getPixelAt(x, y);
190 | if (pixel !== null) return (pixel);
191 | };
192 | return (null);
193 | };
194 |
195 | /**
196 | * Get layer relative pixel
197 | * @param {Number} x
198 | * @param {Number} y
199 | * @return {Array}
200 | */
201 | export function getRelativePixelAt(x, y) {
202 | // normalize coordinates
203 | const bw = this.bounds.w;
204 | const bh = this.bounds.h;
205 | const xx = x - this.bounds.x;
206 | const yy = y - this.bounds.y;
207 | // check if point inside boundings
208 | if (
209 | (xx < 0 || yy < 0) ||
210 | (bw <= 0 || bh <= 0) ||
211 | (xx >= bw || yy >= bh)
212 | ) return (null);
213 | // search for the pixel at given layer
214 | const layer = this.getCurrentLayer();
215 | if (layer !== null) {
216 | return (layer.getPixelAt(x, y));
217 | }
218 | return (null);
219 | };
220 | /**
221 | * Get absolute live pixel
222 | * @param {Number} x
223 | * @param {Number} y
224 | * @return {Array}
225 | */
226 | export function getLivePixelAt(x, y) {
227 | // normalize coordinates
228 | const bw = this.bounds.w;
229 | const bh = this.bounds.h;
230 | const xx = x - this.bounds.x;
231 | const yy = y - this.bounds.y;
232 | // check if point inside boundings
233 | if (
234 | (xx < 0 || yy < 0) ||
235 | (bw <= 0 || bh <= 0) ||
236 | (xx >= bw || yy >= bh)
237 | ) return (null);
238 | // go through each layer reversed
239 | // and search for the given pixel
240 | const layers = this.layers;
241 | for (let ii = 0; ii < layers.length; ++ii) {
242 | const pixel = layers[ii].getLivePixelAt(x, y);
243 | if (pixel !== null) return (pixel);
244 | };
245 | return (null);
246 | };
247 |
248 | export function updateGlobalBoundings() {
249 | const layers = this.layers;
250 | const bounds = this.bounds;
251 | let x = MAX_SAFE_INTEGER; let y = MAX_SAFE_INTEGER;
252 | let w = -MAX_SAFE_INTEGER; let h = -MAX_SAFE_INTEGER;
253 | let count = 0;
254 | for (let ii = 0; ii < layers.length; ++ii) {
255 | const layer = layers[ii];
256 | layer.updateBoundings();
257 | const bounds = layer.bounds;
258 | const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
259 | const bw = bx + bounds.w; const bh = by + bounds.h;
260 | // ignore empty layers
261 | if (bounds.w === 0 && bounds.h === 0) continue;
262 | // calculate x
263 | if (x < 0 && bx < x) x = bx;
264 | else if (x >= 0 && (bx < 0 || bx < x)) x = bx;
265 | // calculate y
266 | if (y < 0 && by < y) y = by;
267 | else if (y >= 0 && (by < 0 || by < y)) y = by;
268 | // calculate width
269 | if (bw > w) w = bw;
270 | // calculate height
271 | if (bh > h) h = bh;
272 | count++;
273 | };
274 | // update our boundings
275 | if (count > 0) {
276 | //this.updateSelectionMatrix();
277 | this.bounds.update(x, y, -x + w, -y + h);
278 | }
279 | };
280 |
281 | /**
282 | * Uses preallocated binary grid with the size of the absolute boundings
283 | * of our working area. In the next step we trace "alive cells" in the grid,
284 | * then we take the boundings of the used area of our grid and crop out
285 | * the relevant part. Next we can process each tile=^2 traced as inside shape
286 | * @param {Number} x
287 | * @param {Number} y
288 | * @param {Array} base
289 | * @return {Uint8Array}
290 | */
291 | export function getBinaryShape(x, y, base) {
292 | const layer = this.getCurrentLayer();
293 | const bounds = layer.bounds;
294 | const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
295 | const bw = bounds.w; const bh = bounds.h;
296 | const isEmpty = base[3] === 0;
297 | const gridl = bw * bh;
298 | // allocate and do a basic fill onto the grid
299 | let grid = new Uint8Array(bw * bh);
300 | for (let ii = 0; ii < gridl; ++ii) {
301 | const xx = ii % bw;
302 | const yy = (ii / bw) | 0;
303 | const color = layer.getPixelAt(bx + xx, by + yy);
304 | // empty tile based
305 | if (isEmpty) { if (color !== null) continue; }
306 | // color based
307 | else {
308 | if (color === null) continue;
309 | if (!(
310 | base[0] === color[0] &&
311 | base[1] === color[1] &&
312 | base[2] === color[2] &&
313 | base[3] === color[3]
314 | )) continue;
315 | }
316 | // fill tiles with 1's if we got a color match
317 | grid[yy * bw + xx] = 1;
318 | };
319 | // trace connected tiles by [x,y]=2
320 | let queue = [{x: x - bx, y: y - by}];
321 | while (queue.length > 0) {
322 | const point = queue.pop();
323 | const x = point.x; const y = point.y;
324 | const idx = y * bw + x;
325 | // set this grid tile to 2, if it got traced earlier as a color match
326 | if (grid[idx] === 1) grid[idx] = 2;
327 | const nn = (y-1) * bw + x;
328 | const ee = y * bw + (x+1);
329 | const ss = (y+1) * bw + x;
330 | const ww = y * bw + (x-1);
331 | if (grid[nn] === 1) queue.push({x, y:y-1});
332 | if (grid[ee] === 1) queue.push({x:x+1, y});
333 | if (grid[ss] === 1) queue.push({x, y:y+1});
334 | if (grid[ww] === 1) queue.push({x:x-1, y});
335 | };
336 | return (grid);
337 | };
338 |
339 | /**
340 | * @return {Number}
341 | */
342 | export function getCursorSize() {
343 | for (let key in this.modes) {
344 | if (!this.modes[key]) continue;
345 | switch (key) {
346 | case "arc":
347 | case "draw":
348 | case "rect":
349 | case "stroke":
350 | return (SETTINGS.PENCIL_SIZE);
351 | break;
352 | case "erase":
353 | return (SETTINGS.ERASER_SIZE);
354 | break;
355 | case "light":
356 | return (SETTINGS.LIGHT_SIZE);
357 | break;
358 | case "move":
359 | case "fill":
360 | case "shape":
361 | case "flood":
362 | case "select":
363 | return (1);
364 | break;
365 | case "pipette":
366 | return (1);
367 | break;
368 | };
369 | };
370 | return (1);
371 | };
372 |
--------------------------------------------------------------------------------
/src/env/insert.js:
--------------------------------------------------------------------------------
1 | import { SETTINGS } from "../cfg";
2 | import { alignToGrid } from "../math";
3 |
4 | import {
5 | getRainbowColor,
6 | alphaByteToRgbAlpha
7 | } from "../color";
8 |
9 | import CommandKind from "../stack/kind";
10 |
11 | /**
12 | * @param {CanvasRenderingContext2D} ctx
13 | * @param {Number} x
14 | * @param {Number} y
15 | * @return {Void}
16 | */
17 | export function insertImage(ctx, x, y) {
18 | const layer = this.getCurrentLayer();
19 | if (layer === null) return;
20 | const batch = layer.createBatchAt(x, y);
21 | const view = ctx.canvas;
22 | const width = view.width; const height = view.height;
23 | const data = ctx.getImageData(0, 0, width, height).data;
24 | const ww = width - 1; const hh = height - 1;
25 | batch.resizeRectangular(
26 | x, y,
27 | width - 1, height - 1
28 | );
29 | let count = 0;
30 | for (let ii = 0; ii < data.length; ii += 4) {
31 | const idx = (ii / 4) | 0;
32 | const xx = (idx % width) | 0;
33 | const yy = (idx / width) | 0;
34 | const px = (yy * width + xx) * 4;
35 | const r = data[px + 0];
36 | const g = data[px + 1];
37 | const b = data[px + 2];
38 | const a = data[px + 3];
39 | if (a <= 0) continue;
40 | batch.drawPixelFast(x + xx, y + yy, [r, g, b, alphaByteToRgbAlpha(a)]);
41 | count++;
42 | };
43 | // nothing changed
44 | if (count <= 0) {
45 | batch.kill();
46 | return;
47 | }
48 | batch.refreshTexture(true);
49 | batch.resizeByMatrixData();
50 | this.enqueue(CommandKind.INSERT_IMAGE, batch);
51 | return;
52 | };
53 |
54 | /**
55 | * @param {Number} x0
56 | * @param {Number} y0
57 | * @param {Number} x1
58 | * @param {Number} y1
59 | */
60 | export function insertLine(x0, y0, x1, y1) {
61 | const base = 8 * this.cr;
62 | const batch = this.getCurrentDrawingBatch();
63 | const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0);
64 | const sx = (x0 < x1) ? 1 : -1; const sy = (y0 < y1) ? 1 : -1;
65 | let err = (dx - dy);
66 | while (true) {
67 | const relative = this.getRelativeTileOffset(x0, y0);
68 | // TODO: limit repeation rate on brush size (take modulo)
69 | if (this.states.drawing) {
70 | batch.drawAt(relative.x, relative.y, SETTINGS.PENCIL_SIZE, this.fillStyle);
71 | }
72 | else if (this.states.erasing) {
73 | batch.clearAt(relative.x, relative.y, SETTINGS.ERASER_SIZE);
74 | }
75 | else if (this.states.lighting) {
76 | batch.applyColorLightness(relative.x, relative.y, SETTINGS.LIGHTING_MODE);
77 | }
78 | else if (this.states.stroke) {
79 | batch.drawAt(x0, y0, SETTINGS.PENCIL_SIZE, this.fillStyle);
80 | }
81 | if (x0 === x1 && y0 === y1) break;
82 | const e2 = 2 * err;
83 | if (e2 > -dy) { err -= dy; x0 += sx; }
84 | if (e2 < dx) { err += dx; y0 += sy; }
85 | };
86 | };
87 |
88 | /**
89 | * Inserts filled arc at given position
90 | * @param {Batch} batch
91 | * @param {Number} x
92 | * @param {Number} y
93 | * @param {Number} radius
94 | * @param {Array} color
95 | */
96 | export function fillArc(batch, x, y, radius, color) {
97 | radius = (radius || 1.0) | 0;
98 | if (!color) color = [255, 255, 255, 1];
99 | this.insertStrokedArc(batch, x, y, radius, color);
100 | // TODO: now fill the stroked circle (with fill?)
101 | };
102 |
103 | /**
104 | * Inserts stroked arc at given position
105 | * @param {Batch} batch
106 | * @param {Number} x
107 | * @param {Number} y
108 | * @param {Number} radius
109 | * @param {Array} color
110 | */
111 | export function strokeArc(batch, x, y, radius, color) {
112 | radius = (radius || 1.0) | 0;
113 | if (!color) color = [255, 255, 255, 1];
114 | this.insertStrokedArc(batch, x, y, radius, color);
115 | };
116 |
117 | /**
118 | * Inserts filled arc at given position
119 | * @param {Batch} batch
120 | * @param {Number} x1
121 | * @param {Number} y1
122 | * @param {Number} radius
123 | * @param {Array} color
124 | */
125 | export function insertStrokedArc(batch, x1, y1, radius, color) {
126 | let x2 = radius;
127 | let y2 = 0;
128 | let err = 0;
129 | const size = SETTINGS.PENCIL_SIZE;
130 | for (;x2 >= y2;) {
131 | batch.drawAt(x2 + x1, y2 + y1, size, color);
132 | batch.drawAt(y2 + x1, x2 + y1, size, color);
133 | batch.drawAt(-x2 + x1, y2 + y1, size, color);
134 | batch.drawAt(-y2 + x1, x2 + y1, size, color);
135 | batch.drawAt(-x2 + x1, -y2 + y1, size, color);
136 | batch.drawAt(-y2 + x1, -x2 + y1, size, color);
137 | batch.drawAt(x2 + x1, -y2 + y1, size, color);
138 | batch.drawAt(y2 + x1, -x2 + y1, size, color);
139 | if (err <= 0) {
140 | y2 += 1;
141 | err += 2 * y2 + 1;
142 | }
143 | if (err > 0) {
144 | x2 -= 1;
145 | err -= 2 * x2 + 1;
146 | }
147 | };
148 | };
149 |
150 | /**
151 | * Inserts filled rectangle at given position
152 | * @param {Batch} batch
153 | * @param {Number} x
154 | * @param {Number} y
155 | * @param {Number} width
156 | * @param {Number} height
157 | * @param {Array} color
158 | */
159 | export function fillRect(batch, x, y, width, height, color) {
160 | if (!color) color = [255, 255, 255, 1];
161 | this.insertRectangleAt(
162 | batch,
163 | x | 0, y | 0,
164 | width | 0, height | 0,
165 | color, true
166 | );
167 | };
168 |
169 | /**
170 | * Inserts stroked rectangle at given position
171 | * @param {Batch} batch
172 | * @param {Number} x
173 | * @param {Number} y
174 | * @param {Number} width
175 | * @param {Number} height
176 | * @param {Array} color
177 | */
178 | export function strokeRect(batch, x, y, width, height, color) {
179 | if (!color) color = [255, 255, 255, 1];
180 | this.insertRectangleAt(
181 | batch,
182 | x | 0, y | 0,
183 | width | 0, height | 0,
184 | color, false
185 | );
186 | };
187 |
188 | /**
189 | * Inserts rectangle at given position
190 | * @param {Batch} batch
191 | * @param {Number} x1
192 | * @param {Number} y1
193 | * @param {Number} x2
194 | * @param {Number} y2
195 | * @param {Array} color
196 | * @param {Boolean} filled
197 | */
198 | export function insertRectangleAt(batch, x1, y1, x2, y2, color, filled) {
199 | const width = Math.abs(x2);
200 | const height = Math.abs(y2);
201 | const dx = (x2 < 0 ? -1 : 1);
202 | const dy = (y2 < 0 ? -1 : 1);
203 | const size = SETTINGS.PENCIL_SIZE;
204 | // stroke rectangle
205 | if (!filled) {
206 | for (let ii = 0; ii < width * height; ++ii) {
207 | const xx = (ii % width) | 0;
208 | const yy = (ii / width) | 0;
209 | if (!(
210 | (xx === 0 || xx >= width-1) ||
211 | (yy === 0 || yy >= height-1))
212 | ) continue;
213 | batch.drawAt(x1 + xx * dx, y1 + yy * dy, size, color);
214 | };
215 | // filled rectangle
216 | } else {
217 | for (let ii = 0; ii < width * height; ++ii) {
218 | const xx = (ii % width) | 0;
219 | const yy = (ii / width) | 0;
220 | batch.drawAt(x1 + xx * dx, y1 + yy * dy, size, color);
221 | };
222 | }
223 | };
224 |
--------------------------------------------------------------------------------
/src/event/emitter.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maierfelix/poxi/08e58c0216e7fda7c975321c1ca75407929f4063/src/event/emitter.js
--------------------------------------------------------------------------------
/src/event/listener.js:
--------------------------------------------------------------------------------
1 | import {
2 | hexToRgba,
3 | rgbaToHex,
4 | rgbaToBytes,
5 | getRainbowColor
6 | } from "../color";
7 |
8 | import {
9 | MODES,
10 | SETTINGS,
11 | TILE_SIZE,
12 | LIGHT_DARKEN_IMG_PATH,
13 | LIGHT_LIGHTEN_IMG_PATH
14 | } from "../cfg";
15 |
16 | import { pointDistance } from "../math";
17 |
18 | import CommandKind from "../stack/kind";
19 |
20 | export function initListeners() {
21 |
22 | window.addEventListener("resize", (e) => this.onResize(e));
23 |
24 | window.addEventListener("mousedown", (e) => this.onMouseDown(e));
25 | window.addEventListener("mouseup", (e) => this.onMouseUp(e));
26 |
27 | window.addEventListener("mousemove", (e) => this.onMouseMove(e));
28 |
29 | window.addEventListener("keydown", (e) => this.onKeyDown(e));
30 | window.addEventListener("keyup", (e) => this.onKeyUp(e));
31 |
32 | window.addEventListener("contextmenu", (e) => this.onContextmenu(e));
33 |
34 | window.addEventListener("wheel", (e) => this.onMouseWheel(e));
35 | window.addEventListener("mousewheel", (e) => this.onMouseWheel(e));
36 |
37 | this.view.addEventListener("mouseout", (e) => this.onMouseOut(e));
38 | this.view.addEventListener("mouseleave", (e) => this.onMouseLeave(e));
39 |
40 | menu.addEventListener("click", (e) => this.onColorMenuClick(e));
41 |
42 | };
43 |
44 | /**
45 | * @param {Event} e
46 | */
47 | export function onResize(e) {
48 | this.resize(
49 | window.innerWidth, window.innerHeight
50 | );
51 | };
52 |
53 | /**
54 | * @param {Event} e
55 | */
56 | export function onMouseOut(e) {
57 | e.preventDefault();
58 | //this.onMouseUp(e);
59 | };
60 |
61 | /**
62 | * @param {Event} e
63 | */
64 | export function onMouseLeave(e) {
65 | e.preventDefault();
66 | //this.onMouseUp(e);
67 | };
68 |
69 | export function resetListElementsActiveState(el) {
70 | for (let ii = 0; ii < el.children.length; ii++) {
71 | el.children[ii].classList.remove("active");
72 | };
73 | };
74 |
75 | /**
76 | * @param {HTMLElement} el
77 | * @return {Void}
78 | */
79 | export function onColorMenuClick(el) {
80 | const element = el.target;
81 | if (element.id) return;
82 | const value = element.getAttribute("color");
83 | const rgba = JSON.parse(value);
84 | this.setUiColorByRgba(rgba);
85 | return;
86 | };
87 |
88 | /**
89 | * @param {HTMLElement} el
90 | */
91 | export function processUIClick(el) {
92 | const parent = el.parentNode;
93 | if (!parent) return;
94 | const id = parent.id;
95 | if (id === "pencil-size") {
96 | const value = el.innerHTML;
97 | SETTINGS.PENCIL_SIZE = parseInt(value);
98 | this.resetModes();
99 | this.modes.draw = true;
100 | tiled.style.opacity = 1.0;
101 | }
102 | else if (id === "eraser-size") {
103 | const value = el.innerHTML;
104 | SETTINGS.ERASER_SIZE = parseInt(value);
105 | this.resetModes();
106 | this.modes.erase = true;
107 | erase.style.opacity = 1.0;
108 | }
109 | else if (id === "light-size") {
110 | const value = el.innerHTML;
111 | SETTINGS.LIGHT_SIZE = parseInt(value);
112 | this.resetModes();
113 | this.modes.light = true;
114 | lighting.style.opacity = 1.0;
115 | }
116 | else return;
117 | this.resetListElementsActiveState(el.parentNode);
118 | el.classList.add("active");
119 | };
120 |
121 | /**
122 | * @param {Event} e
123 | */
124 | export function onMouseDown(e) {
125 | // only allow clicking on canvas
126 | if (!(e.target instanceof HTMLCanvasElement)) {
127 | this.processUIClick(e.target);
128 | return;
129 | }
130 | // finalize earlier operations and abort
131 | if (this.isInActiveState() && e.which === 1) {
132 | this.onMouseUp(e);
133 | return;
134 | }
135 | const x = e.clientX;
136 | const y = e.clientY;
137 | const relative = this.getRelativeTileOffset(x, y);
138 | const rx = relative.x; const ry = relative.y;
139 | if (e.which === 1) {
140 | const layer = this.getCurrentLayer();
141 | if (layer === null) return;
142 | this.resetSelection();
143 | if (this.modes.move) {
144 | const layer = this.getLayerByPoint(rx, ry);
145 | if (layer !== null) {
146 | this.buffers.mLayer = this.getCurrentLayer();
147 | this.setActiveLayer(layer);
148 | this.states.moving = true;
149 | const batch = this.createDynamicBatch(rx, ry);
150 | batch.position.mx = rx;
151 | batch.position.my = ry;
152 | batch.layer = layer;
153 | batch.isMover = true;
154 | this.buffers.move = batch;
155 | }
156 | }
157 | else if (this.modes.select) {
158 | this.states.selecting = true;
159 | this.selectFrom(x, y);
160 | this.selectTo(x, y);
161 | }
162 | else if (this.modes.arc) {
163 | this.states.arc = true;
164 | this.buffers.arc = layer.createBatchAt(rx, ry);
165 | const batch = this.buffers.arc;
166 | batch.forceRendering = true;
167 | batch.refreshTexture(false);
168 | }
169 | else if (this.modes.rect) {
170 | this.states.rect = true;
171 | this.buffers.rect = layer.createBatchAt(rx, ry);
172 | const batch = this.buffers.rect;
173 | batch.forceRendering = true;
174 | batch.refreshTexture(false);
175 | }
176 | else if (this.modes.draw) {
177 | this.states.drawing = true;
178 | this.buffers.drawing = layer.createBatchAt(rx, ry);
179 | const batch = this.buffers.drawing;
180 | batch.forceRendering = true;
181 | batch.drawAt(rx, ry, SETTINGS.PENCIL_SIZE, this.fillStyle);
182 | batch.refreshTexture(false);
183 | }
184 | else if (this.modes.erase) {
185 | this.states.erasing = true;
186 | this.buffers.erasing = layer.createBatchAt(rx, ry);
187 | const batch = this.buffers.erasing;
188 | batch.forceRendering = true;
189 | batch.clearRect(rx, ry, SETTINGS.ERASER_SIZE, SETTINGS.ERASER_SIZE);
190 | batch.refreshTexture(false);
191 | batch.isEraser = true;
192 | }
193 | else if (this.modes.light) {
194 | this.states.lighting = true;
195 | this.buffers.lighting = layer.createBatchAt(rx, ry);
196 | const batch = this.buffers.lighting;
197 | batch.forceRendering = true;
198 | batch.applyColorLightness(rx, ry, SETTINGS.LIGHTING_MODE);
199 | batch.refreshTexture(false);
200 | }
201 | else if (this.modes.stroke) {
202 | this.states.stroke = true;
203 | this.buffers.stroke = layer.createBatchAt(rx, ry);
204 | const batch = this.buffers.stroke;
205 | batch.forceRendering = true;
206 | batch.refreshTexture(false);
207 | }
208 | else if (this.modes.flood) {
209 | this.floodPaint(rx, ry);
210 | }
211 | else if (this.modes.fill) {
212 | this.fillBucket(rx, ry, this.fillStyle);
213 | }
214 | else if (this.modes.shape) {
215 | const batch = this.getShapeAt(rx, ry);
216 | this.shape = batch;
217 | }
218 | else if (this.modes.pipette) {
219 | this.states.pipette = true;
220 | const color = this.getAbsolutePixelAt(rx, ry);
221 | if (color !== null) {
222 | this.fillStyle = color;
223 | color_view.style.background = color.value = rgbaToHex(color);
224 | }
225 | }
226 | }
227 | else if (e.which === 2 || e.which === 3) {
228 | e.preventDefault();
229 | this.states.dragging = true;
230 | this.click(x, y);
231 | }
232 | if (e.which === 1) {
233 | this.last.mdx = x; this.last.mdy = y;
234 | const start = this.getRelativeTileOffset(this.last.mdx, this.last.mdy);
235 | this.last.mdrx = start.x; this.last.mdry = start.y;
236 | }
237 | };
238 |
239 | let lastx = -0;
240 | let lasty = -0;
241 | /**
242 | * @param {Event} e
243 | */
244 | export function onMouseMove(e) {
245 | e.preventDefault();
246 | const x = e.clientX; const y = e.clientY;
247 | if (!(e.target instanceof HTMLCanvasElement)) return;
248 | const last = this.last;
249 | const layer = this.getCurrentLayer();
250 | const relative = this.getRelativeTileOffset(x, y);
251 | const rx = relative.x; const ry = relative.y;
252 | // mouse polling rate isn't 'per-pixel'
253 | // so we try to interpolate missed offsets
254 | this.updateCursorPosition(x, y);
255 | if (this.modes.move) {
256 | this.redraw = true;
257 | }
258 | if (this.states.dragging) {
259 | this.drag(x, y);
260 | this.hover(x, y);
261 | lastx = x; lasty = y;
262 | last.mx = rx; last.my = ry;
263 | return;
264 | }
265 | if (last.mx === rx && last.my === ry) return;
266 | this.hover(x, y);
267 | this.redraw = true;
268 | if (this.states.moving) {
269 | const batch = this.buffers.move;
270 | batch.move(rx, ry);
271 | batch.refreshTexture(false);
272 | }
273 | else if (this.states.arc) {
274 | const batch = this.buffers.arc;
275 | batch.clear();
276 | const sx = this.last.mdrx;
277 | const sy = this.last.mdry;
278 | const radius = pointDistance(sx, sy, rx, ry);
279 | this.strokeArc(batch, sx, sy, radius, this.fillStyle);
280 | batch.refreshTexture(false);
281 | }
282 | else if (this.states.rect) {
283 | const batch = this.buffers.rect;
284 | batch.clear();
285 | const sx = this.last.mdrx;
286 | const sy = this.last.mdry;
287 | const ww = rx - sx;
288 | const hh = ry - sy;
289 | this.strokeRect(batch, sx, sy, ww, hh, this.fillStyle);
290 | batch.refreshTexture(false);
291 | }
292 | else if (this.states.stroke) {
293 | const batch = this.buffers.stroke;
294 | batch.clear();
295 | this.insertLine(this.last.mdrx, this.last.mdry, rx, ry);
296 | batch.refreshTexture(false);
297 | }
298 | else if (this.states.drawing) {
299 | const batch = this.buffers.drawing;
300 | this.insertLine(x, y, lastx, lasty);
301 | batch.refreshTexture(false);
302 | }
303 | else if (this.states.erasing) {
304 | const batch = this.buffers.erasing;
305 | this.insertLine(x, y, lastx, lasty);
306 | batch.refreshTexture(false);
307 | }
308 | else if (this.states.lighting) {
309 | const batch = this.buffers.lighting;
310 | this.insertLine(x, y, lastx, lasty);
311 | batch.refreshTexture(false);
312 | }
313 | else if (this.states.dragging) {
314 | this.drag(x, y);
315 | }
316 | else if (this.states.selecting) {
317 | this.selectTo(x, y);
318 | }
319 | else if (this.states.pipette) {
320 | const color = this.getAbsolutePixelAt(rx, ry);
321 | if (color !== null) {
322 | this.fillStyle = color;
323 | color_view.style.background = color.value = rgbaToHex(color);
324 | }
325 | }
326 | lastx = x; lasty = y;
327 | last.mx = rx; last.my = ry;
328 | };
329 |
330 | /**
331 | * @param {Event} e
332 | */
333 | export function onMouseUp(e) {
334 | e.preventDefault();
335 | if (!(e.target instanceof HTMLCanvasElement)) return;
336 | if (e.which === 1) {
337 | if (this.getCurrentLayer() === null) return;
338 | if (this.modes.move && this.buffers.move) {
339 | const batch = this.buffers.move;
340 | const layer = batch.layer;
341 | this.states.move = false;
342 | this.states.moving = false;
343 | // only enqueue if batch not empty
344 | if (batch.position.x !== 0 || batch.position.y !== 0) {
345 | layer.x -= batch.position.x;
346 | layer.y -= batch.position.y;
347 | this.enqueue(CommandKind.LAYER_MOVE, batch);
348 | } else {
349 | batch.kill();
350 | }
351 | this.setActiveLayer(this.buffers.mLayer);
352 | this.buffers.move = null;
353 | }
354 | else if (this.states.arc) {
355 | const batch = this.buffers.arc;
356 | batch.forceRendering = false;
357 | this.states.arc = false;
358 | batch.resizeByMatrixData();
359 | batch.refreshTexture(false);
360 | if (batch.isEmpty()) batch.kill();
361 | else this.enqueue(CommandKind.ARC_FILL, batch);
362 | this.buffers.arc = null;
363 | }
364 | else if (this.states.rect) {
365 | const batch = this.buffers.rect;
366 | batch.forceRendering = false;
367 | this.states.rect = false;
368 | batch.resizeByMatrixData();
369 | batch.refreshTexture(false);
370 | if (batch.isEmpty()) batch.kill();
371 | else this.enqueue(CommandKind.RECT_FILL, batch);
372 | this.buffers.rect = null;
373 | }
374 | else if (this.states.stroke) {
375 | const batch = this.buffers.stroke;
376 | batch.forceRendering = false;
377 | this.states.stroke = false;
378 | batch.resizeByMatrixData();
379 | batch.refreshTexture(false);
380 | if (batch.isEmpty()) batch.kill();
381 | else this.enqueue(CommandKind.STROKE, batch);
382 | this.buffers.stroke = null;
383 | }
384 | else if (this.states.select) {
385 | this.states.selecting = false;
386 | this.redraw = true;
387 | }
388 | else if (this.states.drawing) {
389 | const batch = this.buffers.drawing;
390 | batch.forceRendering = false;
391 | batch.resizeByMatrixData();
392 | this.states.drawing = false;
393 | this.enqueue(CommandKind.DRAW, batch);
394 | this.buffers.drawing = null;
395 | }
396 | else if (this.states.erasing) {
397 | const batch = this.buffers.erasing;
398 | batch.forceRendering = false;
399 | batch.resizeByMatrixData();
400 | this.states.erasing = false;
401 | if (batch.isEmpty()) batch.kill();
402 | else this.enqueue(CommandKind.ERASE, batch);
403 | this.buffers.erasing = null;
404 | }
405 | else if (this.states.lighting) {
406 | const batch = this.buffers.lighting;
407 | batch.forceRendering = false;
408 | batch.resizeByMatrixData();
409 | this.states.lighting = false;
410 | if (batch.isEmpty()) batch.kill();
411 | else this.enqueue(CommandKind.LIGHTING, batch);
412 | this.buffers.lighting = null;
413 | }
414 | else if (this.states.pipette) {
415 | this.states.pipette = false;
416 | }
417 | }
418 | if (e.which === 2 || e.which === 3) {
419 | this.states.dragging = false;
420 | }
421 | };
422 |
423 | /**
424 | * @param {Event} e
425 | */
426 | export function onKeyDown(e) {
427 | const code = e.keyCode;
428 | const target = e.target;
429 | if (target !== document.body) {
430 | return;
431 | }
432 | e.preventDefault();
433 | this.keys[code] = 1;
434 | switch (code) {
435 | // ctrl
436 | case 17:
437 | if (this.modes.light) {
438 | // lighting mode is darken
439 | SETTINGS.LIGHTING_MODE = -(Math.abs(SETTINGS.LIGHTING_MODE));
440 | lighting.src = LIGHT_DARKEN_IMG_PATH;
441 | }
442 | break;
443 | // del
444 | case 46:
445 | this.clearSelection(this.getSelection());
446 | this.resetSelection();
447 | break;
448 | // c | ctrl+c
449 | case 67:
450 | if (this.keys[17]) {
451 | this.copy(this.getSelection());
452 | }
453 | break;
454 | // x | ctrl+x
455 | case 88:
456 | if (this.keys[17]) {
457 | this.cut(this.getSelection());
458 | this.resetSelection();
459 | }
460 | break;
461 | // v + ctrl+v
462 | case 86:
463 | if (this.keys[17]) {
464 | this.pasteAt(this.last.mx, this.last.my, this.getSelection());
465 | this.resetSelection();
466 | }
467 | break;
468 | // z | ctr+z
469 | case 90:
470 | if (this.keys[17]) {
471 | this.undo();
472 | }
473 | break;
474 | // y | ctrl+y
475 | case 89:
476 | if (this.keys[17]) {
477 | this.redo();
478 | }
479 | break;
480 | // f2
481 | case 113:
482 | MODES.DEV = !MODES.DEV;
483 | this.redraw = true;
484 | break;
485 | // f5
486 | case 116:
487 | location.reload();
488 | break;
489 | // space
490 | case 32:
491 | // already open, close so
492 | if (this.states.fastColorMenu) {
493 | this.closeFastColorPickerMenu();
494 | return;
495 | }
496 | const width = menu.clientWidth;
497 | const height = menu.clientHeight;
498 | const btmWidth = document.querySelector(".bottom-menu").clientHeight;
499 | let yy = lasty;
500 | let xx = (lastx - (width / 2) | 0);
501 | // invert menu position since we are out of window view
502 | if (yy + height > stage.ch - btmWidth) {
503 | yy = yy - height;
504 | }
505 | menu.style.top = yy + "px";
506 | menu.style.left = xx + "px";
507 | this.openFastColorPickerMenu();
508 | break;
509 | default:
510 | return;
511 | break;
512 | };
513 | };
514 |
515 | /**
516 | * @param {Event} e
517 | */
518 | export function onKeyUp(e) {
519 | e.preventDefault();
520 | const code = e.keyCode;
521 | this.keys[code] = 0;
522 | switch (code) {
523 | // ctrl
524 | case 17:
525 | // lighting mode is lighten
526 | if (this.modes.light) {
527 | SETTINGS.LIGHTING_MODE = Math.abs(SETTINGS.LIGHTING_MODE);
528 | lighting.src = LIGHT_LIGHTEN_IMG_PATH;
529 | }
530 | break;
531 | };
532 | };
533 |
534 | /**
535 | * @param {Event} e
536 | */
537 | export function onContextmenu(e) {
538 | e.preventDefault();
539 | };
540 |
541 | /**
542 | * @param {Event} e
543 | */
544 | export function onMouseWheel(e) {
545 | if (!(e.target instanceof HTMLCanvasElement)) return;
546 | e.preventDefault();
547 | const x = e.clientX;
548 | const y = e.clientY;
549 | const value = e.deltaY > 0 ? -1 : 1;
550 | this.click(x, y);
551 | this.scale(value);
552 | this.hover(x, y);
553 | };
554 |
--------------------------------------------------------------------------------
/src/extend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {Class} cls
3 | * @param {Array} prot
4 | */
5 | export default function(cls, prot) {
6 | for (let key in prot) {
7 | if (prot[key] instanceof Function) {
8 | if (cls.prototype[key] instanceof Function) {
9 | console.log(`Warning: Overwriting ${cls.name}.prototype.${key}`);
10 | }
11 | cls.prototype[key] = prot[key];
12 | }
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import extend from "./extend";
2 |
3 | import * as _select from "./area/select";
4 | import * as _area_functions from "./area/functions";
5 |
6 | import * as _camera from "./camera/functions";
7 |
8 | import * as _emitter from "./event/emitter";
9 | import * as _listener from "./event/listener";
10 |
11 | import * as _env from "./env/functions";
12 | import * as _fill from "./env/fill";
13 | import * as _insert from "./env/insert";
14 |
15 | import * as _buffer from "./render/buffer";
16 | import * as _build from "./render/build";
17 | import * as _draw from "./render/draw";
18 | import * as _generate from "./render/generate";
19 | import * as _main from "./render/main";
20 | import * as _render from "./render/render";
21 | import * as _resize from "./render/resize";
22 | import * as _shaders from "./render/shaders";
23 |
24 | import * as _redo from "./stack/redo";
25 | import * as _state from "./stack/state";
26 | import * as _undo from "./stack/undo";
27 |
28 | import * as _read from "./storage/read";
29 | import * as _write from "./storage/write";
30 |
31 | import * as _transflip from "./transform/flip";
32 | import * as _transrotate from "./transform/rotate";
33 |
34 | import * as _ui from "./ui/index";
35 |
36 | import * as _setup from "./setup";
37 |
38 | import { BASE_SCALE } from "./cfg";
39 |
40 | import Layer from "./Layer/index";
41 | import Boundings from "./bounds/index";
42 |
43 | /**
44 | * @class {Poxi}
45 | */
46 | class Poxi {
47 | /**
48 | * @constructor
49 | */
50 | constructor() {
51 | // # webgl related
52 | // wgl context
53 | this.gl = null;
54 | // canvas reference
55 | this.view = null;
56 | // webgl program
57 | this.program = null;
58 | // global boundings
59 | this.bounds = new Boundings();
60 | // # camera related
61 | this.cx = 0;
62 | this.cy = 0;
63 | this.cw = 0;
64 | this.ch = 0;
65 | // camera render scale
66 | this.cr = BASE_SCALE;
67 | this.cs = BASE_SCALE;
68 | // camera drag related
69 | this.dx = 0;
70 | this.dy = 0;
71 | // camera zoom related
72 | this.lx = 0;
73 | this.ly = 0;
74 | // selection related
75 | this.sx = 0;
76 | this.sy = 0;
77 | this.sw = -0;
78 | this.sh = -0;
79 | this.shape = null;
80 | // mouse offset
81 | this.mx = 0;
82 | this.my = 0;
83 | // stack related
84 | this.stack = [];
85 | this.sindex = -1;
86 | // layer related
87 | this.layers = [];
88 | // container related
89 | this.containers = [];
90 | // general cache
91 | this.cache = {
92 | bg: null,
93 | fg: null,
94 | // unused base layer
95 | layer: null,
96 | fgTexture: null,
97 | grid: null,
98 | gridTexture: null,
99 | // wgl cache
100 | gl: {
101 | // empty texture
102 | empty: null,
103 | // general buffers
104 | buffers: {},
105 | // we use buffered uv coords
106 | vertices: {},
107 | // texture pool
108 | textures: {}
109 | }
110 | };
111 | // last things
112 | this.last = {
113 | cx: 1, cy: 1,
114 | // mouse move coordinates
115 | mx: 0, my: 0,
116 | // mouse down coordinates
117 | mdx: 0, mdy: 0,
118 | // mouse down relative coordinates
119 | mdrx: 0, mdry: 0
120 | };
121 | // shared buffer related
122 | this.buffers = {
123 | arc: null,
124 | rect: null,
125 | move: null,
126 | stroke: null,
127 | erasing: null,
128 | drawing: null
129 | };
130 | // keyboard related
131 | this.keys = {};
132 | // clipboard related
133 | this.clipboard = {
134 | copy: null
135 | };
136 | // stage stages
137 | this.states = {
138 | arc: false,
139 | rect: false,
140 | stroke: false,
141 | moving: false,
142 | drawing: false,
143 | pipette: false,
144 | lighting: false,
145 | dragging: false,
146 | select: false,
147 | selecting: false,
148 | fastColorMenu: false
149 | };
150 | // mode related
151 | this.modes = {
152 | arc: false,
153 | move: false,
154 | fill: false,
155 | rect: false,
156 | draw: false,
157 | shape: false,
158 | light: false,
159 | erase: false,
160 | flood: false,
161 | select: false,
162 | stroke: false,
163 | pipette: false
164 | };
165 | // how many frames we have drawn
166 | this.frames = 0;
167 | // indicates if we have to redraw our stage
168 | this.redraw = false;
169 | // global fill style
170 | this.fillStyle = [0, 0, 0, 0];
171 | // favorite used colors
172 | this.favoriteColors = [];
173 | // selected active layer
174 | this.activeLayer = null;
175 | this.setup();
176 | }
177 | };
178 |
179 | extend(Poxi, _select);
180 | extend(Poxi, _area_functions);
181 |
182 | extend(Poxi, _camera);
183 |
184 | extend(Poxi, _emitter);
185 | extend(Poxi, _listener);
186 |
187 | extend(Poxi, _env);
188 | extend(Poxi, _fill);
189 | extend(Poxi, _insert);
190 |
191 | extend(Poxi, _buffer);
192 | extend(Poxi, _build);
193 | extend(Poxi, _draw);
194 | extend(Poxi, _generate);
195 | extend(Poxi, _main);
196 | extend(Poxi, _render);
197 | extend(Poxi, _resize);
198 | extend(Poxi, _shaders);
199 |
200 | extend(Poxi, _redo);
201 | extend(Poxi, _state);
202 | extend(Poxi, _undo);
203 |
204 | extend(Poxi, _read);
205 | extend(Poxi, _write);
206 |
207 | extend(Poxi, _transflip);
208 | extend(Poxi, _transrotate);
209 |
210 | extend(Poxi, _ui);
211 |
212 | extend(Poxi, _setup);
213 |
214 | if (typeof window !== "undefined") {
215 | window.Poxi = Poxi;
216 | window.stage = new Poxi();
217 | } else {
218 | throw new Error("Poxi only runs inside the browser");
219 | }
220 |
--------------------------------------------------------------------------------
/src/layer/README.md:
--------------------------------------------------------------------------------
1 | # Layers
2 |
3 | Layers contain batches and update their boundings as soon as a batch gets added or resized. Layers also store a xy position and opacity value. Layers can also be hidden, locked and re-named.
4 |
--------------------------------------------------------------------------------
/src/layer/index.js:
--------------------------------------------------------------------------------
1 | import extend from "../extend";
2 | import {
3 | uid,
4 | createCanvasBuffer
5 | } from "../utils";
6 | import { getRainbowColor } from "../color";
7 |
8 | import Batch from "../batch/index";
9 | import Boundings from "../bounds/index";
10 |
11 | import * as _matrix from "./matrix";
12 |
13 | /**
14 | * @class {Layer}
15 | */
16 | class Layer {
17 | /**
18 | * @param {Poxi} instance
19 | * @constructor
20 | */
21 | constructor(instance) {
22 | this.id = uid();
23 | this.instance = instance;
24 | // position
25 | this.x = 0;
26 | this.y = 0;
27 | // references layers inference colors
28 | this.color = { value: null };
29 | // last boundings
30 | this.last = { x: 0, y: 0, w: 0, h: 0 };
31 | // we can name layers
32 | this.index = this.generateLayerNameIndex();
33 | this._name = "Layer " + this.index;
34 | // reference to ui node
35 | this.node = null;
36 | // reference (clone) to master layer
37 | this.reference = null;
38 | // opacity applied over local batches
39 | this._opacity = 1.0;
40 | // layer batch matrix
41 | this.batch = null;
42 | // batches we hold here
43 | this.batches = [];
44 | // relative boundings
45 | this.bounds = new Boundings();
46 | // layer states get/set base
47 | this._visible = true;
48 | this._locked = false;
49 | this.allocateLayerMatrix();
50 | }
51 | /**
52 | * @return {String}
53 | */
54 | get name() {
55 | return (this._name);
56 | }
57 | /**
58 | * @param {String}
59 | */
60 | set name(value) {
61 | this._name = value;
62 | const node = this.node.querySelector(".layer-text");
63 | node.value = value;
64 | }
65 | /**
66 | * @return {Number}
67 | */
68 | get opacity() {
69 | return (this._opacity);
70 | }
71 | /**
72 | * @param {Number}
73 | */
74 | set opacity(value) {
75 | this._opacity = value;
76 | this.instance.redraw = true;
77 | }
78 | /**
79 | * @return {Boolean}
80 | */
81 | get visible() {
82 | return (this._visible);
83 | }
84 | /**
85 | * @param {Boolean} state
86 | */
87 | set visible(state) {
88 | this._visible = state;
89 | this.instance.redraw = true;
90 | const node = this.node.querySelector(".layer-item-visible");
91 | node.src = state ? "assets/img/visible.png" : "assets/img/invisible.png";
92 | }
93 | /**
94 | * @return {Boolean}
95 | */
96 | get locked() {
97 | return (this._locked);
98 | }
99 | /**
100 | * @param {Boolean} state
101 | */
102 | set locked(state) {
103 | this._locked = state;
104 | this.instance.redraw = true;
105 | const node = this.node.querySelector(".layer-item-locked");
106 | node.src = state ? "assets/img/locked.png" : "assets/img/unlocked.png";
107 | }
108 | /**
109 | * Returns if layer is active
110 | * @return {Boolean}
111 | */
112 | get isActive() {
113 | const current = this.instance.getCurrentLayer();
114 | return (current === this);
115 | }
116 | /**
117 | * Indicates if layer is a reference (absolute or referenced)
118 | * @return {Boolean}
119 | */
120 | get isReference() {
121 | return (
122 | (this.getReferencedLayers().length > 0 || this.reference !== null)
123 | );
124 | }
125 | };
126 |
127 | /**
128 | * @return {Layer}
129 | */
130 | Layer.prototype.clone = function() {
131 | const layer = new Layer(this.instance);
132 | const batch = this.batch.clone();
133 | layer.last = Object.assign(layer.last);
134 | layer.opacity = this.opacity;
135 | layer.bounds = this.bounds.clone();
136 | layer.x = this.x; layer.y = this.y;
137 | layer.batch = batch;
138 | layer.batches.push(batch);
139 | layer._visible = this.visible;
140 | layer._locked = this.locked;
141 | return (layer);
142 | };
143 |
144 | Layer.prototype.cloneByReference = function() {
145 | if (this.color.value === null) {
146 | this.color.value = getRainbowColor();
147 | this.removeUiReference();
148 | this.addUiReference();
149 | }
150 | const layer = new Layer(this.instance);
151 | layer.last = Object.assign(layer.last);
152 | layer.opacity = this.opacity;
153 | layer.bounds = this.bounds;
154 | layer.x = this.x; layer.y = this.y;
155 | layer.batch = this.batch;
156 | layer.batches = this.batches;
157 | //layer.reference = this.reference || this;
158 | layer.reference = this;
159 | layer.color = this.color;
160 | layer._visible = this.visible;
161 | layer._locked = this.locked;
162 | return (layer);
163 | };
164 |
165 | /**
166 | * Walks through referenced layers until
167 | * the absolute layer reference is found
168 | * @return {Layer}
169 | */
170 | Layer.prototype.getAbsoluteReference = function() {
171 | let layer = this.reference;
172 | while (true) {
173 | if (layer.reference === null) {
174 | return (layer);
175 | }
176 | layer = layer.reference;
177 | };
178 | return (null);
179 | };
180 |
181 | /**
182 | * @return {Array}
183 | */
184 | Layer.prototype.getReferencedLayers = function() {
185 | const layers = this.instance.layers;
186 | const references = [];
187 | for (let ii = 0; ii < layers.length; ++ii) {
188 | const layer = layers[ii];
189 | if (layer === this) continue;
190 | if (layer.reference !== null) {
191 | if (layer.reference === this) references.push(layer);
192 | }
193 | };
194 | return (references);
195 | };
196 |
197 | Layer.prototype.allocateLayerMatrix = function() {
198 | const instance = this.instance;
199 | this.batch = instance.createDynamicBatch(0, 0);
200 | // add reference to unused layer so we can use
201 | // the batch matrix logic for our layers too
202 | // but without including layer x,y in calculations
203 | this.batch.layer = instance.cache.layer;
204 | };
205 |
206 | /**
207 | * @return {Number}
208 | */
209 | Layer.prototype.getIndex = function() {
210 | const layers = this.instance.layers;
211 | for (let ii = 0; ii < layers.length; ++ii) {
212 | const layer = layers[ii];
213 | if (this.id === layer.id) return (ii);
214 | };
215 | return (-1);
216 | };
217 |
218 | /**
219 | * @return {Boolean}
220 | */
221 | Layer.prototype.isEmpty = function() {
222 | return (this.batch.isEmpty());
223 | };
224 |
225 | Layer.prototype.removeFromLayers = function() {
226 | const layers = this.instance.layers;
227 | for (let ii = 0; ii < layers.length; ++ii) {
228 | const layer = layers[ii];
229 | if (this.id === layer.id) layers.splice(ii, 1);
230 | };
231 | };
232 |
233 | /**
234 | * @param {Number} x
235 | * @param {Number} y
236 | * @return {Batch}
237 | */
238 | Layer.prototype.createBatchAt = function(x, y) {
239 | const dx = (x - this.x) | 0;
240 | const dy = (y - this.y) | 0;
241 | const batch = new Batch(this.instance);
242 | batch.prepareMatrix(dx, dy);
243 | this.addBatch(batch);
244 | return (batch);
245 | };
246 |
247 | /**
248 | * Push batch and auto update layer boundings
249 | * @param {Batch} batch
250 | */
251 | Layer.prototype.addBatch = function(batch) {
252 | batch.layer = this;
253 | this.batches.push(batch);
254 | };
255 |
256 | /**
257 | * @param {Number} id
258 | * @return {Number}
259 | */
260 | Layer.prototype.getBatchById = function(id) {
261 | let result = null;
262 | const batches = this.batches;
263 | for (let ii = 0; ii < batches.length; ++ii) {
264 | const batch = batches[ii];
265 | if (batch.id === id) {
266 | result = batch;
267 | break;
268 | }
269 | };
270 | return (result);
271 | };
272 |
273 | /**
274 | * Returns the layer position
275 | * relative to our stage
276 | * @return {Object}
277 | */
278 | Layer.prototype.getRelativePosition = function() {
279 | const sx = this.instance.bounds.x;
280 | const sy = this.instance.bounds.y;
281 | const x = (this.x + this.bounds.x) - sx;
282 | const y = (this.y + this.bounds.y) - sy;
283 | return ({ x, y });
284 | };
285 |
286 | /**
287 | * Fill imagedata with pixels then
288 | * put it into a canvas and return it
289 | * @return {CanvasRenderingContext2D}
290 | */
291 | Layer.prototype.toCanvasBuffer = function() {
292 | // TODO: also draw batch[n].forceRendering for live previews
293 | const data = this.batch.data;
294 | const lw = this.bounds.w | 0;
295 | const lh = this.bounds.h | 0;
296 | const buffer = createCanvasBuffer(lw, lh);
297 | // prevent imagedata construction from failing
298 | if (lw <= 0 || lh <= 0) return (buffer);
299 | const img = new ImageData(lw, lh);
300 | const idata = img.data;
301 | for (let ii = 0; ii < data.length; ii += 4) {
302 | const alpha = data[ii + 3] | 0;
303 | if (alpha <= 0) continue;
304 | idata[ii + 0] = data[ii + 0] | 0;
305 | idata[ii + 1] = data[ii + 1] | 0;
306 | idata[ii + 2] = data[ii + 2] | 0;
307 | idata[ii + 3] = alpha;
308 | };
309 | buffer.putImageData(img, 0, 0);
310 | return (buffer);
311 | };
312 |
313 | /**
314 | * Auto generates a layer index name
315 | * and filles missing layer indices
316 | * @return {Number}
317 | */
318 | Layer.prototype.generateLayerNameIndex = function() {
319 | const layers = this.instance.layers;
320 | // clone, take numeric index, sort ascending, es6 left its muck here
321 | const sorted = layers.concat().map((item) => item.index).sort((a, b) => a - b);
322 | for (let ii = 0; ii < sorted.length; ++ii) {
323 | if (sorted.indexOf(ii) < 0) return (ii);
324 | };
325 | return (layers.length);
326 | };
327 |
328 | /**
329 | * Returns the string version of the dom node
330 | * @return {String}
331 | */
332 | Layer.prototype.generateUiNode = function() {
333 | const html = `
334 |
335 |
336 |
337 |
338 |
339 | `;
340 | return (html);
341 | };
342 |
343 | /**
344 | * Returns color of the layer node
345 | * @return {String}
346 | */
347 | Layer.prototype.getUiNodeColor = function() {
348 | // only attach color to layer if
349 | // layer is a absolute reference or is a reference
350 | const count = this.isReference;
351 | const cc = this.color.value;
352 | const color = (
353 | cc && count ? `rgba(${cc[0]},${cc[1]},${cc[2]},0.1)` : ""
354 | );
355 | return (color);
356 | };
357 |
358 | /**
359 | * yuck in here, yuck in here
360 | */
361 | Layer.prototype.addUiReference = function() {
362 | const tmpl = this.generateUiNode();
363 | const parser = new DOMParser();
364 | const html = parser.parseFromString(tmpl, "text/html").querySelector(".layer-item");
365 | const index = this.getIndex();
366 | const ctx = window.layers;
367 | if (index >= ctx.children.length) {
368 | ctx.appendChild(html);
369 | } else {
370 | ctx.insertBefore(html, ctx.children[index]);
371 | }
372 | // save reference to inserted layer node
373 | this.node = html;
374 | html.style.backgroundColor = this.getUiNodeColor();
375 | if (this.isActive) {
376 | this.instance.setActiveLayer(this);
377 | }
378 | this.locked = this.locked;
379 | this.visible = this.visible;
380 | };
381 |
382 | Layer.prototype.removeUiReference = function() {
383 | this.node.parentNode.removeChild(this.node);
384 | this.node = null;
385 | };
386 |
387 | extend(Layer, _matrix);
388 |
389 | export default Layer;
390 |
--------------------------------------------------------------------------------
/src/layer/matrix.js:
--------------------------------------------------------------------------------
1 | import { MAX_SAFE_INTEGER } from "../cfg";
2 | import { alphaByteToRgbAlpha } from "../color";
3 |
4 | /**
5 | * @return {Boolean}
6 | */
7 | export function hasResized() {
8 | const ox = this.bounds.x; const oy = this.bounds.y;
9 | const ow = this.bounds.w; const oh = this.bounds.h;
10 | const nx = this.last.x; const ny = this.last.y;
11 | const nw = this.last.w; const nh = this.last.h;
12 | return (
13 | ox !== nx || oy !== ny ||
14 | ow !== nw || oh !== nh
15 | );
16 | };
17 |
18 | /**
19 | * Updates the layer's boundings by calculating
20 | * min/max of x,y,w,h of all stored batches
21 | */
22 | export function updateBoundings() {
23 | let x = MAX_SAFE_INTEGER; let y = MAX_SAFE_INTEGER;
24 | let w = -MAX_SAFE_INTEGER; let h = -MAX_SAFE_INTEGER;
25 | const batches = this.batches;
26 | let count = 0;
27 | for (let ii = 0; ii < batches.length; ++ii) {
28 | const batch = batches[ii];
29 | const bounds = batch.bounds;
30 | const bx = bounds.x; const by = bounds.y;
31 | const bw = bx + bounds.w; const bh = by + bounds.h;
32 | // ignore empty batches
33 | if (bounds.w === 0 && bounds.h === 0) continue;
34 | // calculate x
35 | if (x < 0 && bx < x) x = bx;
36 | else if (x >= 0 && (bx < 0 || bx < x)) x = bx;
37 | // calculate y
38 | if (y < 0 && by < y) y = by;
39 | else if (y >= 0 && (by < 0 || by < y)) y = by;
40 | // calculate width
41 | if (bw > w) w = bw;
42 | // calculate height
43 | if (bh > h) h = bh;
44 | count++;
45 | };
46 | // update our boundings
47 | if (count > 0) {
48 | const bounds = this.bounds;
49 | this.last.x = bounds.x; this.last.y = bounds.y;
50 | this.last.w = bounds.w; this.last.h = bounds.h;
51 | bounds.update(
52 | x, y,
53 | -x + w, -y + h
54 | );
55 | }
56 | if (this.hasResized()) {
57 | const main = this.batch;
58 | main.bounds.update(
59 | this.bounds.x, this.bounds.y,
60 | this.bounds.w, this.bounds.h
61 | );
62 | const xx = this.last.x; const yy = this.last.y;
63 | const ww = this.last.w; const hh = this.last.h;
64 | main.resizeMatrix(
65 | xx - this.bounds.x, yy - this.bounds.y,
66 | this.bounds.w - ww, this.bounds.h - hh
67 | );
68 | }
69 | };
70 |
71 | /**
72 | * Access raw pixel
73 | * @param {Number} x
74 | * @param {Number} y
75 | * @return {Array}
76 | */
77 | export function getPixelAt(x, y) {
78 | const bw = this.bounds.w;
79 | const bh = this.bounds.h;
80 | // normalize coordinates
81 | const dx = (x - this.x) | 0;
82 | const dy = (y - this.y) | 0;
83 | const xx = dx - this.bounds.x;
84 | const yy = dy - this.bounds.y;
85 | // check if point inside boundings
86 | if (
87 | (xx < 0 || yy < 0) ||
88 | (bw <= 0 || bh <= 0) ||
89 | (xx >= bw || yy >= bh)
90 | ) return (null);
91 | // now get the pixel from the layer matrix
92 | return (this.batch.getRawPixelAt(dx, dy));
93 | };
94 |
95 | /**
96 | * Access live pixel
97 | * @param {Number} x
98 | * @param {Number} y
99 | * @return {Array}
100 | */
101 | export function getLivePixelAt(x, y) {
102 | const bw = this.bounds.w;
103 | const bh = this.bounds.h;
104 | // normalize coordinates
105 | const dx = (x - this.x) | 0;
106 | const dy = (y - this.y) | 0;
107 | const xx = dx - this.bounds.x;
108 | const yy = dy - this.bounds.y;
109 | // check if point inside boundings
110 | if (
111 | (xx < 0 || yy < 0) ||
112 | (bw <= 0 || bh <= 0) ||
113 | (xx >= bw || yy >= bh)
114 | ) return (null);
115 | for (let ii = 0; ii < this.batches.length; ++ii) {
116 | const idx = (this.batches.length - 1) - ii;
117 | const batch = this.batches[idx];
118 | const pixel = batch.getRawPixelAt(dx, dy);
119 | if (pixel !== null) return (pixel);
120 | };
121 | return (null);
122 | };
123 |
124 | /**
125 | * Merges two layers
126 | * Resize this by layer<->this bounding diff
127 | * Inject this matrix into layer matrix at layer bound pos
128 | * @param {Layer} olayer
129 | * @return {Batch}
130 | */
131 | export function mergeWithLayer(olayer) {
132 | const layer = olayer.clone();
133 | // TODO: fix merging referenced layers
134 | const main = this.batch;
135 | const opacity = this.opacity;
136 | const ldata = layer.batch.data;
137 | const lw = layer.bounds.w;
138 | const lh = layer.bounds.h;
139 | const lx = layer.x + layer.bounds.x;
140 | const ly = layer.y + layer.bounds.y;
141 | const sx = this.x + this.bounds.x;
142 | const sy = this.y + this.bounds.y;
143 | const dx = sx - lx; const dy = sy - ly;
144 | const bx = sx - dx; const by = sy - dy;
145 | const batch = this.createBatchAt(bx, by);
146 | // pre-resize batch
147 | batch.bounds.w = lw;
148 | batch.bounds.h = lh;
149 | // allocate pixel memory
150 | batch.data = new Uint8Array(ldata.length);
151 | batch.reverse = new Uint8Array(ldata.length);
152 | // draw batch matrix into layer matrix
153 | // and save earlier state
154 | for (let ii = 0; ii < ldata.length; ii += 4) {
155 | const idx = (ii / 4) | 0;
156 | const xx = (idx % lw) | 0;
157 | const yy = (idx / lw) | 0;
158 | const pixel = layer.getPixelAt(lx + xx, ly + yy);
159 | if (pixel === null) continue;
160 | //console.log(pixel[3], layer.oapcity);
161 | pixel[3] = pixel[3] * (opacity * layer.opacity);
162 | batch.drawPixelFast(lx + xx, ly + yy, pixel);
163 | };
164 | batch.refreshTexture(true);
165 | return (batch);
166 | };
167 |
--------------------------------------------------------------------------------
/src/math.js:
--------------------------------------------------------------------------------
1 | import { TILE_SIZE } from "./cfg";
2 |
3 | /**
4 | * @param {Number} x
5 | * @return {Number}
6 | */
7 | export function zoomScale(x) {
8 | return (
9 | x >= 0 ? x + 1 :
10 | x < 0 ? x + 1 :
11 | x + 1
12 | );
13 | };
14 |
15 | /**
16 | * @param {Number} x
17 | * @return {Boolean}
18 | */
19 | export function isPowerOfTwo(x) {
20 | return ((x & (x - 1)) === 0);
21 | };
22 |
23 | /**
24 | * @param {Number} x
25 | * @param {Number} t
26 | * @return {Number}
27 | */
28 | export function roundTo(x, t) {
29 | const i = 1 / t;
30 | return (Math.round(x * i) / i);
31 | };
32 |
33 | /**
34 | * @param {Number} value
35 | * @return {Number}
36 | */
37 | export function alignToGrid(value) {
38 | return (roundTo(value, TILE_SIZE));
39 | };
40 |
41 | /**
42 | * @param {Number} x1
43 | * @param {Number} y1
44 | * @param {Number} x2
45 | * @param {Number} y2
46 | * @return {Number}
47 | */
48 | export function pointDistance(x1, y1, x2, y2) {
49 | const xx = Math.pow(x2 - x1, 2);
50 | const yy = Math.pow(y2 - y1, 2);
51 | return (Math.sqrt(xx + yy));
52 | };
53 |
54 | /**
55 | * @param {Number} x1
56 | * @param {Number} y1
57 | * @param {Number} w1
58 | * @param {Number} h1
59 | * @param {Number} x2
60 | * @param {Number} y2
61 | * @param {Number} w2
62 | * @param {Number} h2
63 | * @return {Boolean}
64 | */
65 | export function intersectRectangles(x1, y1, w1, h1, x2, y2, w2, h2) {
66 | x1 = x1 | 0; y1 = y1 | 0;
67 | w1 = w1 | 0; h1 = h1 | 0;
68 | x2 = x2 | 0; y2 = y2 | 0;
69 | w2 = w2 | 0; h2 = h2 | 0;
70 | const xx = Math.max(x1, x2);
71 | const ww = Math.min(x1 + w1, x2 + w2);
72 | const yy = Math.max(y1, y2);
73 | const hh = Math.min(y1 + h1, y2 + h2);
74 | return (ww >= xx && hh >= yy);
75 | };
76 |
--------------------------------------------------------------------------------
/src/render/buffer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create texture buffer from canvas
3 | * @param {String} name
4 | * @param {Uint8Array} data
5 | * @param {Number} width
6 | * @param {Number} height
7 | * @return {WebGLTexture}
8 | */
9 | export function bufferTexture(name, data, width, height) {
10 | const gl = this.gl;
11 | const texture = gl.createTexture();
12 | gl.bindTexture(gl.TEXTURE_2D, texture);
13 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
14 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
15 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
16 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
17 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
18 | if (this.cache.gl.textures[name] === void 0) {
19 | this.cache.gl.textures[name] = texture;
20 | }
21 | gl.bindTexture(gl.TEXTURE_2D, null);
22 | return (this.cache.gl.textures[name]);
23 | };
24 |
25 | /**
26 | * Lookup for the texture inside our texture pool and free it from memory
27 | * @param {WebGLTexture} texture
28 | */
29 | export function destroyTexture(texture) {
30 | const gl = this.gl;
31 | const textures = this.cache.gl.textures;
32 | for (let key in textures) {
33 | let txt = textures[key];
34 | if (txt !== texture) continue;
35 | gl.deleteTexture(txt);
36 | delete textures[key];
37 | txt = null;
38 | break;
39 | };
40 | };
41 |
42 | /**
43 | * @param {WebGLTexture} texture
44 | * @param {Uint8Array} data
45 | * @param {Number} width
46 | * @param {Number} height
47 | */
48 | export function updateTexture(texture, data, width, height) {
49 | const gl = this.gl;
50 | gl.bindTexture(gl.TEXTURE_2D, texture);
51 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
52 | gl.bindTexture(gl.TEXTURE_2D, null);
53 | };
54 |
55 | /**
56 | * Create texture buffer from canvas
57 | * @param {String} name
58 | * @param {HTMLCanvasElement} canvas
59 | * @return {WebGLTexture}
60 | */
61 | export function bufferTextureByCanvas(name, canvas) {
62 | const gl = this.gl;
63 | const texture = gl.createTexture();
64 | gl.bindTexture(gl.TEXTURE_2D, texture);
65 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
66 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
67 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
68 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
69 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
70 | if (this.cache.gl.textures[name] === void 0) {
71 | this.cache.gl.textures[name] = texture;
72 | }
73 | gl.bindTexture(gl.TEXTURE_2D, null);
74 | return (this.cache.gl.textures[name]);
75 | };
76 |
77 | /**
78 | * @param {WebGLTexture} texture
79 | * @param {HTMLCanvasElement} canvas
80 | */
81 | export function updateTextureByCanvas(texture, canvas) {
82 | const gl = this.gl;
83 | gl.bindTexture(gl.TEXTURE_2D, texture);
84 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
85 | gl.bindTexture(gl.TEXTURE_2D, null);
86 | };
87 |
--------------------------------------------------------------------------------
/src/render/build.js:
--------------------------------------------------------------------------------
1 | import { getWGLContext } from "../utils";
2 | import { WGL_TEXTURE_LIMIT } from "../cfg";
3 |
4 | import {
5 | SPRITE_VERTEX,
6 | SPRITE_FRAGMENT
7 | } from "./shaders";
8 |
9 | /**
10 | * @param {HTMLCanvasElement} view
11 | */
12 | export function setupRenderer(view) {
13 | this.view = view;
14 | this.gl = getWGLContext(view);
15 | this.program = this.createSpriteProgram();
16 | this.gl.useProgram(this.program);
17 | this.cache.gl.empty = this.createEmptyTexture();
18 | };
19 |
20 | /**
21 | * @return {WebGLTexture}
22 | */
23 | export function createEmptyTexture() {
24 | const gl = this.gl;
25 | const texture = gl.createTexture();
26 | gl.bindTexture(gl.TEXTURE_2D, texture);
27 | gl.texImage2D(
28 | gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
29 | new Uint8Array([0, 0, 0, 0])
30 | );
31 | gl.bindTexture(gl.TEXTURE_2D, null);
32 | return (texture);
33 | };
34 |
35 | /**
36 | * @return {WebGLProgram}
37 | */
38 | export function createSpriteProgram() {
39 | const gl = this.gl;
40 | const size = WGL_TEXTURE_LIMIT;
41 | const program = gl.createProgram();
42 | const vshader = gl.createShader(gl.VERTEX_SHADER);
43 | const fshader = gl.createShader(gl.FRAGMENT_SHADER);
44 |
45 | this.compileShader(vshader, SPRITE_VERTEX);
46 | this.compileShader(fshader, SPRITE_FRAGMENT);
47 |
48 | gl.attachShader(program, vshader);
49 | gl.attachShader(program, fshader);
50 | gl.linkProgram(program);
51 |
52 | const cache = this.cache.gl;
53 | const buffers = cache.buffers;
54 | const vertices = cache.vertices;
55 | const idxs = vertices.idx = new Float32Array(size * 6);
56 | vertices.position = new Float32Array(size * 12);
57 |
58 | buffers.idx = gl.createBuffer();
59 | buffers.position = gl.createBuffer();
60 | for (let ii = 0; ii < size; ii++) {
61 | idxs[6 * ii + 0] = 0;
62 | idxs[6 * ii + 1] = 1;
63 | idxs[6 * ii + 2] = 2;
64 | idxs[6 * ii + 3] = 1;
65 | idxs[6 * ii + 4] = 2;
66 | idxs[6 * ii + 5] = 3;
67 | };
68 |
69 | this.setGlAttribute(program, buffers.idx, "aIdx", 1, idxs);
70 | return (program);
71 | };
72 |
73 | export function compileShader(shader, shader_src) {
74 | const gl = this.gl;
75 | gl.shaderSource(shader, shader_src);
76 | gl.compileShader(shader);
77 | };
78 |
79 | export function setGlAttribute(program, buffer, name, size, values) {
80 | const gl = this.gl;
81 | const attribute = gl.getAttribLocation(program, name);
82 | gl.enableVertexAttribArray(attribute);
83 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
84 | if (values.length > 0) {
85 | gl.bufferData(gl.ARRAY_BUFFER, values, gl.DYNAMIC_DRAW);
86 | }
87 | gl.vertexAttribPointer(attribute, size, gl.FLOAT, false, 0, 0);
88 | };
89 |
--------------------------------------------------------------------------------
/src/render/draw.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Clears the context
3 | */
4 | export function clear() {
5 | const gl = this.gl;
6 | gl.clearColor(0, 0, 0, 1);
7 | gl.clear(gl.COLOR_BUFFER_BIT);
8 | };
9 |
10 | /**
11 | * Draw a texture
12 | * @param {Texture} tex
13 | * @param {Number} dx
14 | * @param {Number} dy
15 | * @param {Number} dw
16 | * @param {Number} dh
17 | */
18 | export function drawImage(tex, dx, dy, dw, dh) {
19 | dx = dx | 0;
20 | dy = dy | 0;
21 | dw = dw | 0;
22 | dh = dh | 0;
23 |
24 | const gl = this.gl;
25 | const program = this.program;
26 |
27 | gl.uniform2f(
28 | gl.getUniformLocation(program, "uObjScale"),
29 | dw, dh
30 | );
31 |
32 | const pos = this.cache.gl.vertices.position;
33 | for (let ii = 0; ii < 6; ++ii) {
34 | pos[2 * ii + 0] = dx + (dw / 2);
35 | pos[2 * ii + 1] = dy + (dh / 2);
36 | };
37 |
38 | gl.activeTexture(gl.TEXTURE0);
39 | gl.bindTexture(gl.TEXTURE_2D, tex);
40 | this.setGlAttribute(program, this.cache.gl.buffers.position, "aObjCen", 2, pos);
41 | gl.drawArrays(gl.TRIANGLES, 0, 6);
42 |
43 | };
44 |
45 | /**
46 | * Draw a rectangle
47 | * @param {Number} dx
48 | * @param {Number} dy
49 | * @param {Number} dw
50 | * @param {Number} dh
51 | * @param {Array} color
52 | */
53 | export function drawRectangle(dx, dy, dw, dh, color) {
54 | dx = dx | 0;
55 | dy = dy | 0;
56 | dw = dw | 0;
57 | dh = dh | 0;
58 |
59 | const gl = this.gl;
60 | const program = this.program;
61 |
62 | gl.uniform2f(
63 | gl.getUniformLocation(program, "uObjScale"),
64 | dw, dh
65 | );
66 | gl.uniform1i(gl.getUniformLocation(program, "isRect"), 1);
67 |
68 | const pos = this.cache.gl.vertices.position;
69 | for (let ii = 0; ii < 6; ++ii) {
70 | pos[2 * ii + 0] = dx + (dw / 2);
71 | pos[2 * ii + 1] = dy + (dh / 2);
72 | };
73 |
74 | gl.activeTexture(gl.TEXTURE0);
75 | gl.bindTexture(gl.TEXTURE_2D, this.cache.gl.empty);
76 | gl.uniform4f(
77 | gl.getUniformLocation(program, "vColor"),
78 | color[0], color[1], color[2], color[3]
79 | );
80 | this.setGlAttribute(program, this.cache.gl.buffers.position, "aObjCen", 2, pos);
81 | gl.drawArrays(gl.TRIANGLES, 0, 6);
82 | gl.uniform1i(gl.getUniformLocation(program, "isRect"), 0);
83 |
84 | };
85 |
--------------------------------------------------------------------------------
/src/render/generate.js:
--------------------------------------------------------------------------------
1 | import {
2 | TILE_SIZE,
3 | HIDE_GRID,
4 | MAGIC_SCALE,
5 | GRID_LINE_WIDTH
6 | } from "../cfg";
7 |
8 | import {
9 | createCanvasBuffer,
10 | applyImageSmoothing
11 | } from "../utils";
12 |
13 | /**
14 | * @return {CanvasRenderingContext2D}
15 | */
16 | export function createGridBuffer() {
17 | const cw = this.cw;
18 | const ch = this.ch;
19 | const buffer = createCanvasBuffer(cw, ch);
20 | if (this.cache.grid !== null) {
21 | this.cache.grid = null;
22 | this.destroyTexture(this.cache.gridTexture);
23 | }
24 | this.cache.grid = buffer;
25 | this.cache.gridTexture = this.bufferTextureByCanvas("grid", buffer.canvas);
26 | this.redrawGridBuffer();
27 | return (buffer);
28 | };
29 |
30 | /**
31 | * @return {Void}
32 | */
33 | export function redrawGridBuffer() {
34 | if (this.cr <= HIDE_GRID) return;
35 | const buffer = this.cache.grid;
36 | const texture = this.cache.gridTexture;
37 | const cr = this.cr;
38 | const size = (TILE_SIZE * cr) | 0;
39 | const cx = this.cx;
40 | const cy = this.cy;
41 | const cw = this.cw;
42 | const ch = this.ch;
43 | buffer.clearRect(0, 0, cw, ch);
44 | buffer.lineWidth = GRID_LINE_WIDTH;
45 | buffer.strokeStyle = "rgba(51,51,51,0.5)";
46 | buffer.beginPath();
47 | for (let xx = (cx % size) | 0; xx < cw; xx += size) {
48 | buffer.moveTo(xx, 0);
49 | buffer.lineTo(xx, ch);
50 | };
51 | for (let yy = (cy % size) | 0; yy < ch; yy += size) {
52 | buffer.moveTo(0, yy);
53 | buffer.lineTo(cw, yy);
54 | };
55 | buffer.stroke();
56 | buffer.stroke();
57 | buffer.closePath();
58 | this.updateTextureByCanvas(texture, buffer.canvas);
59 | this.last.cx = this.cx;
60 | this.last.cy = this.cy;
61 | return;
62 | };
63 |
64 | /**
65 | * @return {WebGLTexture}
66 | */
67 | export function createBackgroundBuffer() {
68 | if (this.cache.bg instanceof WebGLTexture) {
69 | this.destroyTexture(this.cache.bg);
70 | }
71 | const size = TILE_SIZE;
72 | const cw = this.cw;
73 | const ch = this.ch;
74 | const canvas = document.createElement("canvas");
75 | const buffer = canvas.getContext("2d");
76 | canvas.width = cw;
77 | canvas.height = ch;
78 | // dark rectangles
79 | buffer.fillStyle = "#1f1f1f";
80 | buffer.fillRect(0, 0, cw, ch);
81 | // bright rectangles
82 | buffer.fillStyle = "#212121";
83 | for (let yy = 0; yy < ch; yy += size*2) {
84 | for (let xx = 0; xx < cw; xx += size*2) {
85 | // applied 2 times to increase saturation
86 | buffer.fillRect(xx, yy, size, size);
87 | buffer.fillRect(xx, yy, size, size);
88 | };
89 | };
90 | for (let yy = size; yy < ch; yy += size*2) {
91 | for (let xx = size; xx < cw; xx += size*2) {
92 | buffer.fillRect(xx, yy, size, size);
93 | };
94 | };
95 | const texture = this.bufferTextureByCanvas("background", canvas);
96 | return (texture);
97 | };
98 |
99 | /**
100 | * @return {CanvasRenderingContext2D}
101 | */
102 | export function createForegroundBuffer() {
103 | const cw = this.cw;
104 | const ch = this.ch;
105 | const buffer = createCanvasBuffer(cw, ch);
106 | applyImageSmoothing(buffer, false);
107 | if (this.cache.fg !== null) {
108 | this.cache.fg = null;
109 | this.destroyTexture(this.cache.fgTexture);
110 | }
111 | this.cache.fg = buffer;
112 | this.cache.fgTexture = this.bufferTextureByCanvas("foreground", buffer.canvas);
113 | return (buffer);
114 | };
115 |
--------------------------------------------------------------------------------
/src/render/main.js:
--------------------------------------------------------------------------------
1 | import { createCanvasBuffer } from "../utils";
2 |
3 | /**
4 | * @return {String}
5 | */
6 | export function exportAsDataUrl() {
7 | const layers = this.layers;
8 | const ww = this.bounds.w; const hh = this.bounds.h;
9 | const sx = this.bounds.x; const sy = this.bounds.y;
10 | const buffer = createCanvasBuffer(ww, hh);
11 | const view = buffer.canvas;
12 | for (let ii = 0; ii < layers.length; ++ii) {
13 | const idx = layers.length - 1 - ii;
14 | const layer = layers[idx];
15 | const pos = layer.getRelativePosition();
16 | const canvas = layer.toCanvas();
17 | buffer.drawImage(
18 | canvas,
19 | 0, 0,
20 | canvas.width, canvas.height,
21 | pos.x, pos.y,
22 | canvas.width, canvas.height
23 | );
24 | };
25 | return (view.toDataURL("image/png"));
26 | };
27 |
--------------------------------------------------------------------------------
/src/render/render.js:
--------------------------------------------------------------------------------
1 | import {
2 | MODES,
3 | TILE_SIZE,
4 | HIDE_GRID,
5 | MAGIC_SCALE,
6 | GRID_LINE_WIDTH,
7 | SELECTION_COLOR,
8 | ERASE_TILE_COLOR,
9 | TILE_HOVER_COLOR,
10 | SELECTION_COLOR_ACTIVE
11 | } from "../cfg";
12 |
13 | import { createCanvasBuffer } from "../utils";
14 | import { colorToRgbaString } from "../color";
15 | import { intersectRectangles } from "../math";
16 |
17 | export function update() {
18 | const containers = this.containers;
19 | for (let ii = 0; ii < containers.length; ++ii) {
20 | const container = containers[ii];
21 | const ww = container.bounds.w;
22 | const hh = container.bounds.h;
23 | if (this.frames % 32 === 0) {
24 | const frame = container.animation.frame;
25 | container.animation.frame = (frame >= (ww * hh) - 1) ? 0 : frame + 1;
26 | }
27 | this.redraw = true;
28 | };
29 | };
30 |
31 | export function updateGrid() {
32 | // only redraw texture if it's absolutely necessary
33 | if (this.last.cx !== this.cx || this.last.cy !== this.cy) {
34 | this.redrawGridBuffer();
35 | this.redraw = true;
36 | }
37 | };
38 |
39 | /** Main render method */
40 | export function render() {
41 | this.frames++;
42 | const selection = this.sw !== -0 && this.sh !== -0;
43 | const cr = this.cr;
44 | const gl = this.gl;
45 | const glOpacity = gl.getUniformLocation(this.program, "vOpacity");
46 | gl.uniform1f(
47 | glOpacity, 1.0
48 | );
49 | // clear foreground buffer
50 | this.cache.fg.clearRect(0, 0, this.cw, this.ch);
51 | this.renderBackground();
52 | //if (this.cr > HIDE_GRID) this.renderGrid();
53 | // render cached version of our working area
54 | const cx = this.cx | 0;
55 | const cy = this.cy | 0;
56 | // draw global boundings
57 | if (MODES.DEV) {
58 | const bounds = this.bounds;
59 | const x = (cx + ((bounds.x * TILE_SIZE) * cr)) | 0;
60 | const y = (cy + ((bounds.y * TILE_SIZE) * cr)) | 0;
61 | const w = (bounds.w * TILE_SIZE) * cr;
62 | const h = (bounds.h * TILE_SIZE) * cr;
63 | this.drawRectangle(
64 | x, y,
65 | w, h,
66 | [0, 1, 0, 0.1]
67 | );
68 | }
69 | // render containers
70 | const containers = this.containers;
71 | for (let ii = 0; ii < containers.length; ++ii) {
72 | const container = containers[ii];
73 | if (!container.visible) continue;
74 | this.renderContainer(container);
75 | };
76 | // render layers
77 | const layers = this.layers;
78 | for (let ii = 0; ii < layers.length; ++ii) {
79 | const idx = layers.length - 1 - ii;
80 | const layer = layers[idx];
81 | if (!layer.visible) continue;
82 | gl.uniform1f(
83 | glOpacity, layer.opacity
84 | );
85 | const bounds = layer.bounds;
86 | const ww = (bounds.w * TILE_SIZE) * cr;
87 | const hh = (bounds.h * TILE_SIZE) * cr;
88 | const xx = cx + ((layer.x + bounds.x) * TILE_SIZE) * cr;
89 | const yy = cy + ((layer.y + bounds.y) * TILE_SIZE) * cr;
90 | this.drawImage(
91 | layer.batch.texture,
92 | xx, yy,
93 | ww, hh
94 | );
95 | // don't forget to render live batches
96 | this.renderLayer(layer);
97 | };
98 | gl.uniform1f(
99 | glOpacity, 1.0
100 | );
101 | if (!this.states.drawing && (!this.states.select || !selection)) {
102 | this.renderHoveredTile();
103 | }
104 | if (this.shape !== null) this.renderShapeSelection();
105 | else if (selection) this.renderSelection();
106 | if (MODES.DEV) this.renderStats();
107 | // render foreground buffer
108 | this.renderForeground();
109 | this.redraw = false;
110 | };
111 |
112 | /**
113 | * @param {Layer} layer
114 | */
115 | export function renderLayer(layer) {
116 | const cx = this.cx | 0;
117 | const cy = this.cy | 0;
118 | const cr = this.cr;
119 | const gl = this.gl;
120 | const glOpacity = gl.getUniformLocation(this.program, "vOpacity");
121 | const batches = layer.batches;
122 | const sindex = this.sindex;
123 | for (let ii = 0; ii < batches.length; ++ii) {
124 | const batch = batches[ii];
125 | const bounds = batch.bounds;
126 | if (!batch.forceRendering) continue;
127 | // batch index is higher than stack index, so ignore this batch
128 | if (sindex - batch.getStackIndex() < 0) {
129 | if (!batch.forceRendering) continue;
130 | }
131 | gl.uniform1f(
132 | glOpacity, layer.opacity
133 | );
134 | //if (!this.boundsInsideView(bounds)) continue;
135 | const x = (cx + (((layer.x + bounds.x) * TILE_SIZE) * cr)) | 0;
136 | const y = (cy + (((layer.y + bounds.y) * TILE_SIZE) * cr)) | 0;
137 | const w = (bounds.w * TILE_SIZE) * cr;
138 | const h = (bounds.h * TILE_SIZE) * cr;
139 | // draw batch boundings
140 | if (MODES.DEV) {
141 | this.drawRectangle(
142 | x, y,
143 | w, h,
144 | [1, 0, 0, 0.1]
145 | );
146 | }
147 | // erase by alpha blending
148 | if (batch.isEraser) {
149 | const gl = this.gl;
150 | gl.blendFuncSeparate(gl.ONE_MINUS_SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ZERO);
151 | }
152 | this.drawImage(
153 | batch.texture,
154 | x, y,
155 | w, h
156 | );
157 | // reset blending state
158 | if (batch.isEraser) {
159 | const gl = this.gl;
160 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
161 | }
162 | gl.uniform1f(
163 | glOpacity, 1.0
164 | );
165 | };
166 | // draw a thin rectangle around active layers
167 | if (MODES.DEV && layer.isActive) {
168 | const bounds = layer.bounds;
169 | const x = (cx + (((layer.x + bounds.x) * TILE_SIZE) * cr));
170 | const y = (cy + (((layer.y + bounds.y) * TILE_SIZE) * cr));
171 | const w = (bounds.w * TILE_SIZE) * cr;
172 | const h = (bounds.h * TILE_SIZE) * cr;
173 | this.drawStrokedRect(x, y, w, h, "rgba(255,255,255,0.2)");
174 | }
175 | };
176 |
177 | /**
178 | * @param {Container} container
179 | */
180 | export function renderContainer(container) {
181 | container.renderBoundings();
182 | const ww = container.bounds.w | 0;
183 | const hh = container.bounds.h | 0;
184 | for (let ii = 0; ii < ww * hh; ++ii) {
185 | container.renderFrame(ii);
186 | };
187 | container.renderAnimationPreview();
188 | };
189 |
190 | export function renderForeground() {
191 | const texture = this.cache.fgTexture;
192 | const fg = this.cache.fg;
193 | const view = fg.canvas;
194 | this.updateTextureByCanvas(texture, view);
195 | this.drawImage(
196 | texture,
197 | 0, 0, view.width, view.height
198 | );
199 | };
200 |
201 | export function renderBackground() {
202 | this.drawImage(
203 | this.cache.bg,
204 | 0, 0,
205 | this.cw, this.ch
206 | );
207 | };
208 |
209 | export function renderGrid() {
210 | this.drawImage(
211 | this.cache.gridTexture,
212 | 0, 0,
213 | this.cw, this.ch
214 | );
215 | };
216 |
217 | export function renderHoveredTile() {
218 | const cx = this.cx | 0;
219 | const cy = this.cy | 0;
220 | const cr = this.cr;
221 | // apply empty tile hover color
222 | const mx = this.mx;
223 | const my = this.my;
224 | const relative = this.getRelativeTileOffset(mx, my);
225 | const rx = relative.x * TILE_SIZE;
226 | const ry = relative.y * TILE_SIZE;
227 | const x = ((cx + GRID_LINE_WIDTH/2) + (rx * cr)) | 0;
228 | const y = ((cy + GRID_LINE_WIDTH/2) + (ry * cr)) | 0;
229 | const ww = (TILE_SIZE * cr) | 0;
230 | const hh = (TILE_SIZE * cr) | 0;
231 | this.drawRectangle(
232 | x, y,
233 | ww, hh,
234 | TILE_HOVER_COLOR
235 | );
236 | };
237 |
238 | export function renderSelection() {
239 | const cx = this.cx | 0;
240 | const cy = this.cy | 0;
241 | const cr = this.cr;
242 | const xx = (cx + (this.sx * TILE_SIZE) * cr) | 0;
243 | const yy = (cy + (this.sy * TILE_SIZE) * cr) | 0;
244 | const ww = ((this.sw * TILE_SIZE) * cr) | 0;
245 | const hh = ((this.sh * TILE_SIZE) * cr) | 0;
246 | const color = (
247 | this.states.selecting ?
248 | SELECTION_COLOR_ACTIVE :
249 | SELECTION_COLOR
250 | );
251 | this.drawRectangle(
252 | xx, yy,
253 | ww, hh,
254 | color
255 | );
256 | };
257 |
258 | export function renderShapeSelection() {
259 | const cx = this.cx | 0;
260 | const cy = this.cy | 0;
261 | const cr = this.cr;
262 | const batch = this.shape;
263 | const bounds = batch.bounds;
264 | const xx = (cx + ((bounds.x * TILE_SIZE) * cr)) | 0;
265 | const yy = (cy + ((bounds.y * TILE_SIZE) * cr)) | 0;
266 | const ww = (bounds.w * TILE_SIZE) * cr;
267 | const hh = (bounds.h * TILE_SIZE) * cr;
268 | this.drawImage(
269 | batch.texture,
270 | xx, yy, ww, hh
271 | );
272 | };
273 |
274 | export function renderStats() {
275 | const buffer = this.cache.fg;
276 | const bounds = this.bounds;
277 | const mx = this.last.mx;
278 | const my = this.last.my;
279 | // font style
280 | buffer.font = "10px Verdana";
281 | buffer.fillStyle = "#fff";
282 | // stats
283 | buffer.fillText(`Mouse: x: ${mx}, y: ${my}`, 8, 16);
284 | buffer.fillText(`GPU textures: ${Object.keys(this.cache.gl.textures).length}`, 8, 28);
285 | buffer.fillText(`Boundings: x: ${bounds.x}, y: ${bounds.y}, w: ${bounds.w}, h: ${bounds.h}`, 8, 40);
286 | buffer.fillText(`Camera scale: ${this.cr}`, 8, 52);
287 | buffer.fillText(`Stack: ${this.sindex + 1}:${this.stack.length}`, 8, 64);
288 | // mouse color
289 | let color = this.getAbsolutePixelAt(mx, my);
290 | if (color !== null) {
291 | buffer.fillStyle = colorToRgbaString(color);
292 | buffer.fillRect(8, 70, 8, 8);
293 | buffer.fillStyle = "#fff";
294 | buffer.fillText(`${color[0]}, ${color[1]}, ${color[2]}, ${color[3]}`, 22, 77);
295 | }
296 | /*if (MODES.DEV) {
297 | const bounds = this.bounds;
298 | const cr = this.cr;
299 | const xx = ((this.cx | 0) + ((bounds.x * TILE_SIZE) * cr)) | 0;
300 | const yy = ((this.cy | 0) + ((bounds.y * TILE_SIZE) * cr)) | 0;
301 | const ww = (bounds.w * TILE_SIZE) * cr;
302 | const hh = (bounds.h * TILE_SIZE) * cr;
303 | this.drawResizeRectangle(xx, yy, ww, hh, "#313131");
304 | }*/
305 | if (this.sw !== 0 && this.sh !== 0) {
306 | this.drawSelectionShape();
307 | }
308 | };
309 |
310 | export function drawSelectionShape() {
311 | const cr = this.cr;
312 | const s = this.getSelection();
313 | const xx = ((this.cx | 0) + ((s.x * TILE_SIZE) * cr)) | 0;
314 | const yy = ((this.cy | 0) + ((s.y * TILE_SIZE) * cr)) | 0;
315 | const ww = (s.w * TILE_SIZE) * cr;
316 | const hh = (s.h * TILE_SIZE) * cr;
317 | const size = TILE_SIZE * cr;
318 | const buffer = this.cache.fg;
319 | buffer.strokeStyle = "rgba(255,255,255,0.7)";
320 | buffer.lineWidth = 0.45 * cr;
321 | buffer.setLineDash([size, size]);
322 | buffer.strokeRect(
323 | xx, yy,
324 | ww, hh
325 | );
326 | buffer.setLineDash([0, 0]);
327 | };
328 |
329 | /**
330 | * Draws a stroked rectangle
331 | * @param {Number} x
332 | * @param {Number} y
333 | * @param {Number} w
334 | * @param {Number} h
335 | * @param {String} color
336 | */
337 | export function drawStrokedRect(x, y, w, h, color) {
338 | const cr = this.cr;
339 | const lw = Math.max(0.55, 0.55 * cr);
340 | const buffer = this.cache.fg;
341 | buffer.strokeStyle = color;
342 | buffer.lineWidth = lw;
343 | // main rectangle
344 | buffer.strokeRect(
345 | x - (lw / 2), y - (lw / 2),
346 | w + lw, h + lw
347 | );
348 | };
349 |
350 | /**
351 | * Draw resizable rectangle around given rectangle corners
352 | * @param {Number} x
353 | * @param {Number} y
354 | * @param {Number} w
355 | * @param {Number} h
356 | * @param {String} color
357 | */
358 | export function drawResizeRectangle(x, y, w, h, color) {
359 | const cr = this.cr;
360 | const ww = 4 * cr;
361 | const hh = 4 * cr;
362 | const buffer = this.cache.fg;
363 | buffer.strokeStyle = color;
364 | buffer.lineWidth = Math.max(0.4, 0.45 * cr);
365 | // main rectangle
366 | buffer.strokeRect(
367 | x, y,
368 | w, h
369 | );
370 | buffer.lineWidth = Math.max(0.4, 0.3 * cr);
371 | // left rectangle
372 | buffer.strokeRect(
373 | x - ww, (y + (h / 2) - hh / 2),
374 | ww, hh
375 | );
376 | // right rectangle
377 | buffer.strokeRect(
378 | x + w, (y + (h / 2) - hh / 2),
379 | ww, hh
380 | );
381 | // top rectangle
382 | buffer.strokeRect(
383 | (x + (w / 2) - ww / 2), y - hh,
384 | ww, hh
385 | );
386 | // bottom rectangle
387 | buffer.strokeRect(
388 | (x + (w / 2) - ww / 2), (y + h),
389 | ww, hh
390 | );
391 | };
392 |
--------------------------------------------------------------------------------
/src/render/resize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Resize
3 | * @param {Number} width
4 | * @param {Number} height
5 | */
6 | export function resize(width, height) {
7 | width = width | 0;
8 | height = height | 0;
9 | const gl = this.gl;
10 | const view = this.view;
11 | // first update camera size
12 | this.cw = width;
13 | this.ch = height;
14 | // update view
15 | view.width = width;
16 | view.height = height;
17 | // update viewport
18 | gl.viewport(0, 0, width, height);
19 | // update shader scales
20 | gl.uniform2f(
21 | gl.getUniformLocation(this.program, "uScale"),
22 | width, height
23 | );
24 | gl.enable(gl.BLEND);
25 | gl.disable(gl.CULL_FACE);
26 | gl.disable(gl.DEPTH_TEST);
27 | gl.disable(gl.STENCIL_TEST);
28 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
29 | // re-generate our bg and fg
30 | this.cache.bg = this.createBackgroundBuffer();
31 | this.cache.fg = this.createForegroundBuffer();
32 | // re-generate our grid
33 | this.cache.grid = this.createGridBuffer();
34 | this.redrawGridBuffer();
35 | this.redraw = true;
36 | };
37 |
--------------------------------------------------------------------------------
/src/render/shaders.js:
--------------------------------------------------------------------------------
1 | export const SPRITE_VERTEX = `
2 | precision lowp float;
3 | uniform vec2 uScale;
4 | uniform vec2 uObjScale;
5 | attribute vec2 aObjCen;
6 | attribute float aIdx;
7 | varying vec2 uv;
8 | void main(void) {
9 | if (aIdx == 0.0) {
10 | uv = vec2(0.0,0.0);
11 | } else if (aIdx == 1.0) {
12 | uv = vec2(1.0,0.0);
13 | } else if (aIdx == 2.0) {
14 | uv = vec2(0.0,1.0);
15 | } else {
16 | uv = vec2(1.0,1.0);
17 | }
18 | gl_Position = vec4(
19 | -1.0 + 2.0 * (aObjCen.x + uObjScale.x * (-0.5 + uv.x)) / uScale.x,
20 | 1.0 - 2.0 * (aObjCen.y + uObjScale.y * (-0.5 + uv.y)) / uScale.y,
21 | 0.0, 1.0
22 | );
23 | }
24 | `;
25 |
26 | export const SPRITE_FRAGMENT = `
27 | precision lowp float;
28 | uniform sampler2D uSampler;
29 | varying vec2 uv;
30 | uniform int isRect;
31 | uniform float vOpacity;
32 | uniform vec4 vColor;
33 | void main(void) {
34 | if (isRect == 0) {
35 | gl_FragColor = texture2D(uSampler, uv);
36 | } else {
37 | gl_FragColor = vColor + texture2D(uSampler, uv);
38 | }
39 | gl_FragColor.a *= vOpacity;
40 | if (gl_FragColor.a < 0.01) discard;
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/src/setup.js:
--------------------------------------------------------------------------------
1 | import { rgbaToHex } from "./color";
2 | import { getWGLContext } from "./utils";
3 |
4 | import Batch from "./batch/index";
5 | import Layer from "./layer/index";
6 |
7 | export function setup() {
8 | const view = document.createElement("canvas");
9 | const width = window.innerWidth;
10 | const height = window.innerHeight;
11 | view.width = width;
12 | view.height = height;
13 | // sync storage colors with stage colors
14 | /*const colors = this.readStorage("favorite_colors");
15 | if (colors && colors.length > 2) {
16 | this.favoriteColors = JSON.parse(colors);
17 | this.updateFastColorPickMenu();
18 | this.setUiColorByRgba(this.favoriteColors[0].color);
19 | } else {
20 | this.setUiColorByHex([255, 0, 0, 1]);
21 | }*/
22 | this.setUiColorByRgba([255, 0, 0, 1]);
23 | this.setupRenderer(view);
24 | this.initListeners();
25 | this.resize(width, height);
26 | this.scale(0);
27 | const draw = () => {
28 | requestAnimationFrame(() => draw());
29 | this.update();
30 | if (this.redraw) {
31 | this.clear();
32 | this.render();
33 | }
34 | };
35 | // add some things manually
36 | (() => {
37 | this.cache.layer = new Layer(this);
38 | const layer = this.addLayer();
39 | this.setActiveLayer(layer);
40 | })();
41 | requestAnimationFrame(() => draw());
42 | this.setupUi();
43 | document.body.appendChild(view);
44 | };
45 |
--------------------------------------------------------------------------------
/src/stack/cmd.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @class Command
3 | */
4 | class Command {
5 | /**
6 | * @param {Number} kind
7 | * @param {Batch} batch
8 | * @constructor
9 | */
10 | constructor(kind, batch) {
11 | this.kind = kind;
12 | this.batch = batch;
13 | }
14 | };
15 |
16 | export default Command;
17 |
--------------------------------------------------------------------------------
/src/stack/kind.js:
--------------------------------------------------------------------------------
1 | const CommandKind = {
2 | UNKNOWN: 0,
3 | LAYER_OPERATION: 1,
4 | BATCH_OPERATION: 2,
5 | CONTAINER_OPERATION: 3,
6 | DRAW: 4,
7 | ERASE: 5,
8 | FILL: 6,
9 | BACKGROUND: 7,
10 | PASTE: 8,
11 | CUT: 9,
12 | INSERT_IMAGE: 10,
13 | STROKE: 11,
14 | RECT_FILL: 12,
15 | RECT_STROKE: 13,
16 | ARC_FILL: 14,
17 | ARC_STROKE: 15,
18 | FLOOD_FILL: 16,
19 | LIGHTING: 17,
20 | LAYER_ADD: 18,
21 | LAYER_REMOVE: 19,
22 | LAYER_LOCK: 20,
23 | LAYER_FLIP_VERTICAL: 21,
24 | LAYER_FLIP_HORIZONTAL: 22,
25 | LAYER_MOVE: 23,
26 | LAYER_ORDER: 24,
27 | LAYER_RENAME: 25,
28 | LAYER_ROTATE_LEFT: 26,
29 | LAYER_ROTATE_RIGHT: 27,
30 | LAYER_VISIBILITY: 28,
31 | LAYER_OPACITY: 29,
32 | LAYER_CLONE: 30,
33 | LAYER_CLONE_REF: 31,
34 | LAYER_MERGE: 32,
35 | CONTAINER_ADD: 33,
36 | CONTAINER_REMOVE: 34
37 | };
38 |
39 | export default CommandKind;
40 |
--------------------------------------------------------------------------------
/src/stack/redo.js:
--------------------------------------------------------------------------------
1 | import Command from "./cmd";
2 | import CommandKind from "./kind";
3 |
4 | /**
5 | * @return {Void}
6 | */
7 | export function redo() {
8 | // prevent undo/redo when in e.g drawing state
9 | if (this.isInActiveState()) return;
10 | if (this.sindex < this.stack.length - 1) {
11 | this.sindex++;
12 | const cmd = this.currentStackOperation();
13 | this.fire(cmd, true);
14 | }
15 | this.refreshUiLayers();
16 | this.updateGlobalBoundings();
17 | this.redraw = true;
18 | return;
19 | };
20 |
21 | /**
22 | * @param {Number} kind
23 | * @param {Batch} batch
24 | * @return {Void}
25 | */
26 | export function enqueue(kind, batch) {
27 | // our stack index is out of position
28 | // => clean up all more recent batches
29 | this.refreshStack();
30 | const cmd = new Command(kind, batch);
31 | const type = this.getCommandKind(cmd);
32 | // free the texture from gpu, we dont need it anymore
33 | if (type === CommandKind.BATCH_OPERATION) {
34 | batch.destroyTexture();
35 | }
36 | const last = this.currentStackOperation();
37 | this.stack.push(cmd);
38 | this.redo();
39 | // delete last operation if it's mergable
40 | if (last !== null && last.kind === cmd.kind) {
41 | if (this.isMergeableOperation(cmd.kind)) {
42 | // delete last 2 commands
43 | this.stack.splice(this.sindex - 1, this.stack.length);
44 | this.sindex = this.stack.length - 1;
45 | }
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/src/stack/state.js:
--------------------------------------------------------------------------------
1 | import { MODES } from "../cfg";
2 | import { additiveAlphaColorBlending } from "../color";
3 |
4 | import CommandKind from "./kind";
5 |
6 | /**
7 | * Manually refresh the stack,
8 | * clear future operations etc.
9 | */
10 | export function refreshStack() {
11 | if (this.sindex < this.stack.length - 1) {
12 | this.dequeue(this.sindex, this.stack.length - 1);
13 | } else {
14 | this.stack.splice(this.sindex + 1, this.stack.length);
15 | }
16 | //this.updateGlobalBoundings();
17 | };
18 |
19 | /**
20 | * Returns the latest stack operation
21 | * @return {Command}
22 | */
23 | export function currentStackOperation() {
24 | return (this.stack[this.sindex] || null);
25 | };
26 |
27 | /**
28 | * Indicates if an operation is mergeable
29 | * with an older same kind but opposite operation
30 | * @param {Number} kind
31 | * @return {Boolean}
32 | */
33 | export function isMergeableOperation(kind) {
34 | return (
35 | kind === CommandKind.LAYER_LOCK ||
36 | kind === CommandKind.LAYER_FLIP_VERTICAL ||
37 | kind === CommandKind.LAYER_FLIP_HORIZONTAL ||
38 | kind === CommandKind.LAYER_VISIBILITY
39 | );
40 | };
41 |
42 | /**
43 | * @param {Command} cmd
44 | * @param {Boolean} state
45 | */
46 | export function fire(cmd, state) {
47 | const kind = this.getCommandKind(cmd);
48 | switch (kind) {
49 | case CommandKind.LAYER_OPERATION:
50 | this.fireLayerOperation(cmd, state);
51 | break;
52 | case CommandKind.BATCH_OPERATION:
53 | this.fireBatchOperation(cmd, state);
54 | break;
55 | case CommandKind.CONTAINER_OPERATION:
56 | this.fireContainerOperation(cmd, state);
57 | break;
58 | };
59 | };
60 |
61 | /**
62 | * @param {Command} cmd
63 | * @param {Boolean} state
64 | */
65 | export function fireContainerOperation(cmd, state) {
66 | const batch = cmd.batch;
67 | const container = batch.container;
68 | if (state) {
69 | this.containers.push(container);
70 | } else {
71 | for (let ii = 0; ii < this.containers.length; ++ii) {
72 | if (this.containers[ii].id === container.id) {
73 | this.containers.splice(ii, 1);
74 | break;
75 | }
76 | };
77 | }
78 | };
79 |
80 | /**
81 | * @param {Command} cmd
82 | * @param {Boolean} state
83 | */
84 | export function fireLayerOperation(cmd, state) {
85 | const kind = cmd.kind;
86 | const batch = cmd.batch;
87 | const layer = batch.layer;
88 | const main = layer.batch;
89 | switch (kind) {
90 | case CommandKind.LAYER_MERGE:
91 | layer.updateBoundings();
92 | const data = cmd.batch.data;
93 | if (state) {
94 | layer.removeUiReference();
95 | this.layers.splice(batch.index, 1);
96 | let index = batch.index < 0 ? 0 : batch.index;
97 | index = index === this.layers.length ? index - 1 : index;
98 | const merge = this.getLayerByIndex(index);
99 | this.setActiveLayer(merge);
100 | merge.updateBoundings();
101 | merge.batch.injectMatrix(data, true);
102 | merge.batch.refreshTexture(true);
103 | } else {
104 | const merge = this.getLayerByIndex(batch.index);
105 | this.layers.splice(batch.index, 0, layer);
106 | layer.addUiReference();
107 | this.setActiveLayer(layer);
108 | merge.batch.injectMatrix(data, false);
109 | merge.batch.refreshTexture(true);
110 | }
111 | break;
112 | case CommandKind.LAYER_CLONE:
113 | case CommandKind.LAYER_CLONE_REF:
114 | case CommandKind.LAYER_ADD:
115 | if (state) {
116 | this.layers.splice(batch.index, 0, layer);
117 | layer.addUiReference();
118 | this.setActiveLayer(layer);
119 | } else {
120 | layer.removeUiReference();
121 | this.layers.splice(batch.index, 1);
122 | const index = batch.index < 0 ? 0 : batch.index;
123 | this.setActiveLayer(this.getLayerByIndex(index));
124 | }
125 | break;
126 | case CommandKind.LAYER_REMOVE:
127 | if (!state) {
128 | this.layers.splice(batch.index, 0, layer);
129 | layer.addUiReference();
130 | this.setActiveLayer(layer);
131 | } else {
132 | layer.removeUiReference();
133 | this.layers.splice(batch.index, 1);
134 | let index = batch.index < 0 ? 0 : batch.index;
135 | index = index === this.layers.length ? index - 1 : index;
136 | this.setActiveLayer(this.getLayerByIndex(index));
137 | }
138 | break;
139 | case CommandKind.LAYER_RENAME:
140 | layer.name = batch[state ? "name": "oname"];
141 | break;
142 | case CommandKind.LAYER_LOCK:
143 | layer.locked = !layer.locked;
144 | break;
145 | case CommandKind.LAYER_VISIBILITY:
146 | layer.visible = !layer.visible;
147 | break;
148 | case CommandKind.LAYER_OPACITY:
149 | layer.opacity = batch[state ? "opacity" : "oopacity"];
150 | break;
151 | case CommandKind.LAYER_ORDER:
152 | if (state) {
153 | const tmp = this.layers[batch.oindex];
154 | this.layers[batch.oindex] = this.layers[batch.index];
155 | this.layers[batch.index] = tmp;
156 | tmp.removeUiReference(); tmp.addUiReference();
157 | this.setActiveLayer(tmp);
158 | } else {
159 | const tmp = this.layers[batch.index];
160 | this.layers[batch.index] = this.layers[batch.oindex];
161 | this.layers[batch.oindex] = tmp;
162 | tmp.removeUiReference(); tmp.addUiReference();
163 | this.setActiveLayer(tmp);
164 | }
165 | break;
166 | case CommandKind.LAYER_MOVE:
167 | layer.updateBoundings();
168 | const dir = state ? 1 : -1;
169 | layer.x += (batch.position.x * dir);
170 | layer.y += (batch.position.y * dir);
171 | break;
172 | case CommandKind.LAYER_FLIP_VERTICAL:
173 | this.flipVertically(layer);
174 | break;
175 | case CommandKind.LAYER_FLIP_HORIZONTAL:
176 | this.flipHorizontally(layer);
177 | break;
178 | case CommandKind.LAYER_ROTATE_LEFT:
179 | if (state) this.rotateLeft(layer);
180 | else this.rotateRight(layer);
181 | break;
182 | case CommandKind.LAYER_ROTATE_RIGHT:
183 | if (state) this.rotateRight(layer);
184 | else this.rotateLeft(layer);
185 | break;
186 | };
187 | };
188 |
189 | /**
190 | * @param {Command} cmd
191 | * @param {Boolean} state
192 | */
193 | export function fireBatchOperation(cmd, state) {
194 | const batch = cmd.batch;
195 | const layer = batch.layer;
196 | const main = layer.batch;
197 | const kind = cmd.kind;
198 | layer.updateBoundings();
199 | main.injectMatrix(batch, state);
200 | main.refreshTexture(true);
201 | };
202 |
203 | /**
204 | * @param {Command} cmd
205 | * @return {Number}
206 | */
207 | export function getCommandKind(cmd) {
208 | const kind = cmd.kind;
209 | switch (kind) {
210 | case CommandKind.LAYER_LOCK:
211 | case CommandKind.LAYER_MOVE:
212 | case CommandKind.LAYER_ORDER:
213 | case CommandKind.LAYER_RENAME:
214 | case CommandKind.LAYER_ROTATE_LEFT:
215 | case CommandKind.LAYER_ROTATE_RIGHT:
216 | case CommandKind.LAYER_VISIBILITY:
217 | case CommandKind.LAYER_OPACITY:
218 | case CommandKind.LAYER_ADD:
219 | case CommandKind.LAYER_REMOVE:
220 | case CommandKind.LAYER_MERGE:
221 | case CommandKind.LAYER_CLONE:
222 | case CommandKind.LAYER_CLONE_REF:
223 | case CommandKind.LAYER_FLIP_VERTICAL:
224 | case CommandKind.LAYER_FLIP_HORIZONTAL:
225 | return (CommandKind.LAYER_OPERATION);
226 | break;
227 | case CommandKind.DRAW:
228 | case CommandKind.ERASE:
229 | case CommandKind.FILL:
230 | case CommandKind.BACKGROUND:
231 | case CommandKind.PASTE:
232 | case CommandKind.CUT:
233 | case CommandKind.INSERT_IMAGE:
234 | case CommandKind.STROKE:
235 | case CommandKind.RECT_FILL:
236 | case CommandKind.RECT_STROKE:
237 | case CommandKind.ARC_FILL:
238 | case CommandKind.ARC_STROKE:
239 | case CommandKind.FLOOD_FILL:
240 | case CommandKind.LIGHTING:
241 | return (CommandKind.BATCH_OPERATION);
242 | break;
243 | case CommandKind.CONTAINER_ADD:
244 | case CommandKind.CONTAINER_REMOVE:
245 | return (CommandKind.CONTAINER_OPERATION);
246 | break;
247 | };
248 | return (CommandKind.UNKNOWN);
249 | };
250 |
--------------------------------------------------------------------------------
/src/stack/undo.js:
--------------------------------------------------------------------------------
1 | import CommandKind from "./kind";
2 |
3 | /**
4 | * @return {Void}
5 | */
6 | export function undo() {
7 | // prevent undo/redo when in e.g drawing state
8 | if (this.isInActiveState()) return;
9 | if (this.sindex >= 0) {
10 | const cmd = this.currentStackOperation();
11 | this.fire(cmd, false);
12 | this.sindex--;
13 | }
14 | this.refreshUiLayers();
15 | this.updateGlobalBoundings();
16 | this.redraw = true;
17 | return;
18 | };
19 |
20 | /**
21 | * Dequeue items from stack
22 | * @param {Number} from
23 | * @param {Number} to
24 | */
25 | export function dequeue(from, to) {
26 | from = from + 1;
27 | const count = (to - (from - 1));
28 | for (let ii = count; ii > 0; --ii) {
29 | const idx = from + ii - 1;
30 | const cmd = this.stack[idx];
31 | const kind = this.getCommandKind(cmd);
32 | switch (kind) {
33 | case CommandKind.BATCH_OPERATION:
34 | cmd.batch.kill();
35 | break;
36 | case CommandKind.LAYER_OPERATION:
37 | if (cmd.kind === CommandKind.LAYER_MERGE) {
38 | cmd.batch.data.kill();
39 | }
40 | break;
41 | };
42 | this.stack.splice(idx, 1);
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/src/storage/read.js:
--------------------------------------------------------------------------------
1 | import {
2 | STORAGE_KEY,
3 | STORAGE_OBJECT
4 | } from "../cfg";
5 |
6 | /**
7 | * @param {String} key
8 | * @return {String}
9 | */
10 | export function readStorage(key) {
11 | const access = `${STORAGE_KEY}::${key}`;
12 | const value = STORAGE_OBJECT.getItem(access);
13 | return (value || "");
14 | };
15 |
--------------------------------------------------------------------------------
/src/storage/write.js:
--------------------------------------------------------------------------------
1 | import {
2 | STORAGE_KEY,
3 | STORAGE_OBJECT
4 | } from "../cfg";
5 |
6 | /**
7 | * @param {String} key
8 | * @param {String} value
9 | */
10 | export function writeStorage(key, value) {
11 | const access = `${STORAGE_KEY}::${key}`;
12 | STORAGE_OBJECT.setItem(access, value);
13 | };
14 |
15 | /**
16 | * @param {String} key
17 | * @param {String} value
18 | */
19 | export function appendStorage(key, value) {
20 | const access = `${STORAGE_KEY}::${key}`;
21 | const base = this.readStorage(key);
22 | this.writeStorage(key, base + value);
23 | };
24 |
--------------------------------------------------------------------------------
/src/transform/flip.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {Layer} layer
3 | */
4 | export function flipHorizontally(layer) {
5 | const batch = layer.batch;
6 | const data = batch.data;
7 | const ww = batch.bounds.w;
8 | const hh = batch.bounds.h;
9 | const pixels = new Uint8Array(data.length);
10 | for (let ii = 0; ii < data.length; ii += 4) {
11 | const idx = (ii / 4) | 0;
12 | const xx = (idx % ww) | 0;
13 | const yy = (idx / ww) | 0;
14 | const opx = 4 * (yy * ww + xx);
15 | const npx = 4 * ((ww - xx) + (yy * ww) - 1);
16 | pixels[opx + 0] = data[npx + 0];
17 | pixels[opx + 1] = data[npx + 1];
18 | pixels[opx + 2] = data[npx + 2];
19 | pixels[opx + 3] = data[npx + 3];
20 | };
21 | batch.data = pixels;
22 | batch.refreshTexture(true);
23 | };
24 |
25 | /**
26 | * @param {Layer} layer
27 | */
28 | export function flipVertically(layer) {
29 | const batch = layer.batch;
30 | const data = batch.data;
31 | const ww = batch.bounds.w;
32 | const hh = batch.bounds.h;
33 | const pixels = new Uint8Array(data.length);
34 | for (let ii = 0; ii < data.length; ii += 4) {
35 | const idx = (ii / 4) | 0;
36 | const xx = (idx % ww) | 0;
37 | const yy = (idx / ww) | 0;
38 | const opx = 4 * (yy * ww + xx);
39 | const npx = 4 * (((hh - yy - 1) * ww) + xx);
40 | pixels[opx + 0] = data[npx + 0];
41 | pixels[opx + 1] = data[npx + 1];
42 | pixels[opx + 2] = data[npx + 2];
43 | pixels[opx + 3] = data[npx + 3];
44 | };
45 | batch.data = pixels;
46 | batch.refreshTexture(true);
47 | };
48 |
--------------------------------------------------------------------------------
/src/transform/rotate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {Layer} layer
3 | */
4 | export function rotateRight(layer) {
5 | const main = layer.batch;
6 | const batch = layer.createBatchAt(
7 | main.bounds.x, main.bounds.y
8 | );
9 | const data = main.data;
10 | const ww = main.bounds.w;
11 | const hh = main.bounds.h;
12 | const pixels = new Uint8Array(data.length);
13 | for (let ii = 0; ii < data.length; ii += 4) {
14 | const idx = (ii / 4) | 0;
15 | const xx = (idx % ww) | 0;
16 | const yy = (idx / ww) | 0;
17 | const opx = 4 * (yy * ww + xx);
18 | const npx = 4 * ((xx * ww) + (hh - yy - 1));
19 | pixels[opx + 0] = data[npx + 0];
20 | pixels[opx + 1] = data[npx + 1];
21 | pixels[opx + 2] = data[npx + 2];
22 | pixels[opx + 3] = data[npx + 3];
23 | };
24 | batch.data = pixels;
25 | batch.bounds.w = hh;
26 | batch.bounds.h = ww;
27 | batch.refreshTexture(true);
28 | main.refreshTexture(true);
29 | layer.updateBoundings();
30 | layer.bounds.w = hh;
31 | layer.bounds.h = ww;
32 | main.bounds.w = hh;
33 | main.bounds.h = ww;
34 | main.data = pixels;
35 | this.updateGlobalBoundings();
36 | };
37 |
38 | export function rotateLeft() {
39 |
40 | };
41 |
--------------------------------------------------------------------------------
/src/ui/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | hexToRgba,
3 | rgbaToHex,
4 | rgbaToBytes
5 | } from "../color";
6 |
7 | import Layer from "../layer/index";
8 | import Container from "../container/index";
9 | import CommandKind from "../stack/kind";
10 |
11 | export function resetModes() {
12 | for (let key in this.modes) {
13 | this.resetSelection();
14 | if (this.modes[key] === true) {
15 | this.resetActiveCursor(key);
16 | }
17 | this.modes[key] = false;
18 | };
19 | this.resetActiveUiButtons();
20 | };
21 |
22 | /**
23 | * @param {String} mode
24 | */
25 | export function resetActiveCursor(mode) {
26 | const el = this.getCursorNodeByMode(mode);
27 | if (el !== null) {
28 | el.style.left = "0px";
29 | el.style.top = "0px";
30 | el.style.display = "none";
31 | }
32 | };
33 |
34 | /**
35 | * Returns the current active mode as a string
36 | * @return {String}
37 | */
38 | export function getActiveMode() {
39 | for (let key in this.modes) {
40 | if (this.modes[key] === true) return (key);
41 | };
42 | return (null);
43 | };
44 |
45 | /**
46 | * @param {String} mode
47 | * @return {HTMLElement}
48 | */
49 | export function getCursorNodeByMode(mode) {
50 | const value = "#c" + mode;
51 | const el = document.querySelector(value);
52 | return (el);
53 | };
54 |
55 | /**
56 | * @param {Number} x
57 | * @param {Number} y
58 | * @return {Void}
59 | */
60 | export function updateCursorPosition(x, y) {
61 | const mode = this.getActiveMode();
62 | if (mode === null) return;
63 | const el = this.getCursorNodeByMode(mode);
64 | if (el === null) return;
65 | el.style.left = x + "px";
66 | el.style.top = (y - 16) + "px";
67 | el.style.display = "block";
68 | return;
69 | };
70 |
71 | export function resetActiveUiButtons() {
72 | arc.style.removeProperty("opacity");
73 | move.style.removeProperty("opacity");
74 | shape.style.removeProperty("opacity");
75 | tiled.style.removeProperty("opacity");
76 | erase.style.removeProperty("opacity");
77 | bucket.style.removeProperty("opacity");
78 | select.style.removeProperty("opacity");
79 | stroke.style.removeProperty("opacity");
80 | pipette.style.removeProperty("opacity");
81 | lighting.style.removeProperty("opacity");
82 | rectangle.style.removeProperty("opacity");
83 | paint_all.style.removeProperty("opacity");
84 | };
85 |
86 | /**
87 | * @param {String} value
88 | * @return {Void}
89 | */
90 | export function setUiColorByHex(value) {
91 | // close fast color picker menu
92 | if (this.states.fastColorMenu) {
93 | this.closeFastColorPickerMenu();
94 | }
95 | color_hex.innerHTML = String(value).toUpperCase();
96 | color_view.style.background = value;
97 | const rgba = hexToRgba(value);
98 | // prevent changing color if it didnt changed
99 | if (
100 | this.fillStyle[0] === rgba[0] &&
101 | this.fillStyle[1] === rgba[1] &&
102 | this.fillStyle[2] === rgba[2] &&
103 | this.fillStyle[3] === rgba[3]
104 | ) return;
105 | this.fillStyle = rgba;
106 | this.addCustomColor(rgba);
107 | return;
108 | };
109 |
110 | /**
111 | * @param {Array} rgba
112 | * @return {Void}
113 | */
114 | export function setUiColorByRgba(rgba) {
115 | const r = rgba[0];
116 | const g = rgba[1];
117 | const b = rgba[2];
118 | const a = rgba[3];
119 | color_view.style.background = `rgba(${r},${g},${b},${a})`;
120 | const hex = rgbaToHex(rgba);
121 | // prevent changing color if it didnt changed
122 | if (
123 | this.fillStyle[0] === r &&
124 | this.fillStyle[1] === g &&
125 | this.fillStyle[2] === b &&
126 | this.fillStyle[3] === a
127 | ) return;
128 | this.fillStyle = rgba;
129 | return;
130 | };
131 |
132 | /**
133 | * @param {Array} rgba
134 | */
135 | export function addCustomColor(rgba) {
136 | const colors = this.favoriteColors;
137 | let count = 0;
138 | for (let ii = 0; ii < colors.length; ++ii) {
139 | const color = colors[ii].color;
140 | const index = colors[ii].index;
141 | // color already saved, increase it's importance
142 | if (
143 | color[0] === rgba[0] &&
144 | color[1] === rgba[1] &&
145 | color[2] === rgba[2] &&
146 | color[3] === rgba[3]
147 | ) {
148 | colors[ii].index += 1;
149 | count++;
150 | //console.log("Found!", color, rgba);
151 | }
152 | };
153 | // color isn't saved yet
154 | if (count <= 0) {
155 | // color limit exceeded, replace less used color with this one
156 | if (colors.length >= 16) {
157 | colors[colors.length - 1].color = color;
158 | colors[colors.length - 1].index += 1;
159 | } else {
160 | // we have to replace the less used color
161 | colors.push({
162 | color: rgba,
163 | index: 0
164 | });
165 | }
166 | }
167 | // resort descending by most used color
168 | colors.sort((a, b) => { return (b.index - a.index); });
169 | // sync with storage
170 | this.writeStorage("favorite_colors", JSON.stringify(colors));
171 | // sync color menu with favorite colors
172 | this.updateFastColorPickMenu();
173 | };
174 |
175 | export function closeFastColorPickerMenu() {
176 | menu.style.visibility = "hidden";
177 | this.states.fastColorMenu = false;
178 | };
179 |
180 | export function openFastColorPickerMenu() {
181 | menu.style.visibility = "visible";
182 | this.states.fastColorMenu = true;
183 | };
184 |
185 | export function updateFastColorPickMenu() {
186 | // first remove all color nodes
187 | for (let ii = 0; ii < 16; ++ii) {
188 | const node = colors.children[0];
189 | if (!node) continue;
190 | node.parentNode.removeChild(node);
191 | };
192 | // now re-insert the updated ones
193 | for (let ii = 0; ii < 16; ++ii) {
194 | const color = this.favoriteColors[ii];
195 | const node = document.createElement("div");
196 | if (color) {
197 | node.setAttribute("color", "[" + color.color + "]");
198 | node.style.background = rgbaToHex(color.color);
199 | } else {
200 | node.setAttribute("color", "[0,0,0,1]");
201 | node.style.background = "#000000";
202 | }
203 | colors.appendChild(node);
204 | };
205 | };
206 |
207 | export function hoverLayer(e) {
208 | const el = e.target;
209 | const kind = el.classList.value;
210 | const parent = (
211 | kind !== "layer-item" ? el.parentNode : el
212 | );
213 | const layer = this.getLayerByNode(parent);
214 | if (layer === null) return;
215 | //const canvas = layer.toCanvas();
216 | };
217 |
218 | /**
219 | * @param {HTMLElement} e
220 | * @param {Boolean} dbl
221 | */
222 | export function clickedLayer(e, dbl) {
223 | const el = e.target;
224 | const kind = el.classList.value;
225 | const parent = (
226 | kind !== "layer-item" ? el.parentNode : el
227 | );
228 | const layer = this.getLayerByNode(parent);
229 | if (layer === null) return;
230 | switch (kind) {
231 | // clicked on layer, set it active
232 | case "layer-text":
233 | if (!dbl) {
234 | this.setActiveLayer(layer);
235 | }
236 | else {
237 | el.removeAttribute("readonly");
238 | el.focus();
239 | el.onkeypress = (e) => {
240 | const code = (e.keyCode ? e.keyCode : e.which);
241 | if (code === 13) el.blur();
242 | };
243 | el.onblur = () => {
244 | const oname = layer.name;
245 | if (!el.value) {
246 | layer.name = oname;
247 | } else {
248 | if (String(oname) !== el.value) {
249 | this.enqueue(CommandKind.LAYER_RENAME, {
250 | oname, name: el.value, layer: layer
251 | });
252 | }
253 | }
254 | el.setAttribute("readonly", "readonly");
255 | };
256 | }
257 | break;
258 | // clicked a layer icon
259 | case "layer-item-visible":
260 | this.enqueue(CommandKind.LAYER_VISIBILITY, {
261 | layer: layer
262 | });
263 | break;
264 | // clicked lock icon
265 | case "layer-item-locked":
266 | this.enqueue(CommandKind.LAYER_LOCK, {
267 | layer: layer
268 | });
269 | break;
270 | };
271 | };
272 |
273 | export function setupUi() {
274 |
275 | // ui
276 | tiled.onclick = (e) => {
277 | this.resetModes();
278 | this.modes.draw = true;
279 | tiled.style.opacity = 1.0;
280 | };
281 | erase.onclick = (e) => {
282 | this.resetModes();
283 | this.modes.erase = true;
284 | erase.style.opacity = 1.0;
285 | };
286 | bucket.onclick = (e) => {
287 | this.resetModes();
288 | this.modes.fill = true;
289 | bucket.style.opacity = 1.0;
290 | };
291 | pipette.onclick = (e) => {
292 | this.resetModes();
293 | this.modes.pipette = true;
294 | pipette.style.opacity = 1.0;
295 | };
296 | select.onclick = (e) => {
297 | this.resetModes();
298 | this.modes.select = true;
299 | select.style.opacity = 1.0;
300 | };
301 | stroke.onclick = (e) => {
302 | this.resetModes();
303 | this.modes.stroke = true;
304 | stroke.style.opacity = 1.0;
305 | };
306 | arc.onclick = (e) => {
307 | this.resetModes();
308 | this.modes.arc = true;
309 | arc.style.opacity = 1.0;
310 | };
311 | rectangle.onclick = (e) => {
312 | this.resetModes();
313 | this.modes.rect = true;
314 | rectangle.style.opacity = 1.0;
315 | };
316 | paint_all.onclick = (e) => {
317 | this.resetModes();
318 | this.modes.flood = true;
319 | paint_all.style.opacity = 1.0;
320 | };
321 | shape.onclick = (e) => {
322 | this.resetModes();
323 | this.modes.shape = true;
324 | shape.style.opacity = 1.0;
325 | };
326 | lighting.onclick = (e) => {
327 | this.resetModes();
328 | this.modes.light = true;
329 | lighting.style.opacity = 1.0;
330 | };
331 | move.onclick = (e) => {
332 | this.resetModes();
333 | this.modes.move = true;
334 | move.style.opacity = 1.0;
335 | };
336 | color.onchange = (e) => {
337 | this.setUiColorByHex(color.value);
338 | };
339 |
340 | undo.onclick = (e) => {
341 | this.undo();
342 | };
343 | redo.onclick = (e) => {
344 | this.redo();
345 | };
346 |
347 | download.onclick = (e) => {
348 | const link = document.createElement("a");
349 | const data = this.exportAsDataUrl();
350 | link.href = data;
351 | link.download = 655321 + ".png";
352 | link.click();
353 | };
354 |
355 | // ## drag&drop images
356 | file.onclick = (e) => { e.preventDefault(); };
357 | file.onchange = (e) => {
358 | file.style.display = "none";
359 | let reader = new FileReader();
360 | reader.onload = (e) => {
361 | if (e.target.result.slice(11, 14) !== "png") {
362 | throw new Error("Invalid image type!");
363 | }
364 | let img = new Image();
365 | let canvas = document.createElement("canvas");
366 | let ctx = canvas.getContext("2d");
367 | img.onload = () => {
368 | canvas.width = img.width;
369 | canvas.height = img.height;
370 | ctx.drawImage(
371 | img,
372 | 0, 0,
373 | img.width, img.height,
374 | 0, 0,
375 | img.width, img.height
376 | );
377 | this.insertImage(ctx, this.last.mx, this.last.my);
378 | file.value = ""; // reassign to allow second files
379 | };
380 | img.src = e.target.result;
381 | };
382 | reader.readAsDataURL(e.target.files[0]);
383 | };
384 | // hidy things for drag & drop input
385 | this.view.addEventListener("dragenter", (e) => {
386 | file.style.display = "block";
387 | });
388 | file.addEventListener("dragleave", (e) => {
389 | file.style.display = "none";
390 | });
391 |
392 | layers.addEventListener("mouseover", (e) => this.hoverLayer(e));
393 | layers.addEventListener("click", (e) => this.clickedLayer(e, false));
394 | layers.addEventListener("dblclick", (e) => this.clickedLayer(e, true));
395 |
396 | add_layer.onclick = (e) => {
397 | const layer = this.getCurrentLayer();
398 | let index = layer ? layer.getIndex() : 0;
399 | index = index < 0 ? 0 : index;
400 | this.enqueue(CommandKind.LAYER_ADD, {
401 | layer: new Layer(this), index
402 | });
403 | };
404 | remove_layer.onclick = (e) => {
405 | const layer = this.getCurrentLayer();
406 | let index = layer ? layer.getIndex() : 0;
407 | index = index < 0 ? 0 : index;
408 | if (layer !== null) this.enqueue(CommandKind.LAYER_REMOVE, {
409 | layer, index
410 | });
411 | this.redraw = true;
412 | };
413 |
414 | move_layer_up.onclick = (e) => {
415 | const layer = this.getCurrentLayer();
416 | if (layer !== null && layer.getIndex() > 0) {
417 | this.enqueue(CommandKind.LAYER_ORDER, {
418 | layer, index: layer.getIndex() - 1, oindex: layer.getIndex()
419 | });
420 | }
421 | this.redraw = true;
422 | };
423 | move_layer_down.onclick = (e) => {
424 | const layer = this.getCurrentLayer();
425 | if (layer !== null && layer.getIndex() < this.layers.length - 1) {
426 | this.enqueue(CommandKind.LAYER_ORDER, {
427 | layer, index: layer.getIndex() + 1, oindex: layer.getIndex()
428 | });
429 | }
430 | this.redraw = true;
431 | };
432 |
433 | clone.onclick = (e) => {
434 | const layer = this.getCurrentLayer();
435 | if (layer !== null) {
436 | let index = layer ? layer.getIndex() : 0;
437 | index = index < 0 ? 0 : index;
438 | this.enqueue(CommandKind.LAYER_CLONE, {
439 | layer: layer.clone(), index
440 | });
441 | }
442 | };
443 |
444 | clone_by_ref.onclick = (e) => {
445 | const layer = this.getCurrentLayer();
446 | if (layer !== null) {
447 | let index = layer ? layer.getIndex() : 0;
448 | index = index < 0 ? 0 : index;
449 | this.enqueue(CommandKind.LAYER_CLONE_REF, {
450 | layer: layer.cloneByReference(), index
451 | });
452 | }
453 | };
454 |
455 | flip_horizontal.onclick = (e) => {
456 | const layer = this.getCurrentLayer();
457 | if (layer !== null && !layer.isEmpty()) {
458 | this.enqueue(CommandKind.LAYER_FLIP_HORIZONTAL, { layer });
459 | }
460 | };
461 | flip_vertical.onclick = (e) => {
462 | const layer = this.getCurrentLayer();
463 | if (layer !== null && !layer.isEmpty()) {
464 | this.enqueue(CommandKind.LAYER_FLIP_VERTICAL, { layer });
465 | }
466 | };
467 |
468 | rotate_right.onclick = (e) => {
469 | const layer = this.getCurrentLayer();
470 | if (layer !== null && !layer.isEmpty()) {
471 | this.enqueue(CommandKind.LAYER_ROTATE_RIGHT, { layer });
472 | }
473 | };
474 | rotate_left.onclick = (e) => {
475 | const layer = this.getCurrentLayer();
476 | if (layer !== null && !layer.isEmpty()) {
477 | this.enqueue(CommandKind.LAYER_ROTATE_LEFT, { layer });
478 | }
479 | };
480 |
481 | merge.onclick = (e) => {
482 | const layer = this.getCurrentLayer();
483 | if (layer !== null && this.layers.length > 1) {
484 | if (layer.getIndex() < this.layers.length - 1) {
485 | const merge = this.getLayerByIndex(layer.getIndex() + 1);
486 | const data = merge.mergeWithLayer(layer);
487 | this.enqueue(CommandKind.LAYER_MERGE, { data, layer, index: layer.getIndex() });
488 | }
489 | }
490 | };
491 |
492 | add_animation.onclick = (e) => {
493 | const container = new Container(this);
494 | const relative = this.getRelativeTileOffset(this.mx, this.my);
495 | container.bounds.x = relative.x; container.bounds.y = relative.y;
496 | this.enqueue(CommandKind.CONTAINER_ADD, {
497 | container: container
498 | });
499 | };
500 |
501 | const layer_opacity = (e) => {
502 | const opacity = 0.5;
503 | const oopacity = layer.opacity;
504 | if (oopacity !== opacity) {
505 | this.enqueue(CommandKind.LAYER_OPACITY, {
506 | oopacity, opacity, layer: layer
507 | });
508 | }
509 | };
510 |
511 | this.modes.draw = true;
512 | tiled.style.opacity = 1.0;
513 |
514 | // setup ui list button states
515 | /*this.processUIClick(document.querySelector("#light-size").children[0]);
516 | this.processUIClick(document.querySelector("#eraser-size").children[0]);
517 | this.processUIClick(document.querySelector("#pencil-size").children[0]);*/
518 |
519 | };
520 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import {
2 | WGL_SUPPORTED,
3 | MAGIC_RGB_A_BYTE
4 | } from "./cfg";
5 |
6 | /**
7 | * Returns unique integer
8 | * @return {Number}
9 | */
10 | let uidx = 0;
11 | export function uid() {
12 | return (++uidx);
13 | };
14 |
15 | /**
16 | * @param {Number} width
17 | * @param {Number} height
18 | * @return {CanvasRenderingContext2D}
19 | */
20 | export function createCanvasBuffer(width, height) {
21 | const canvas = document.createElement("canvas");
22 | canvas.width = width;
23 | canvas.height = height;
24 | const ctx = canvas.getContext("2d");
25 | applyImageSmoothing(ctx, false);
26 | return (ctx);
27 | };
28 |
29 | /**
30 | * @param {CanvasRenderingContext2D} ctx
31 | * @param {Boolean} state
32 | */
33 | export function applyImageSmoothing(ctx, state) {
34 | ctx.imageSmoothingEnabled = state;
35 | ctx.oImageSmoothingEnabled = state;
36 | ctx.msImageSmoothingEnabled = state;
37 | ctx.webkitImageSmoothingEnabled = state;
38 | };
39 |
40 | /**
41 | * @param {String} path
42 | * @param {Function} resolve
43 | */
44 | export function loadImage(path, resolve) {
45 | const img = new Image();
46 | img.addEventListener("load", () => {
47 | resolve(img);
48 | });
49 | img.addEventListener("error", () => {
50 | throw new Error("Failed to load image ressource " + path);
51 | });
52 | img.src = path;
53 | };
54 |
55 | /**
56 | * Creates and returns an webgl context
57 | * @param {HTMLCanvasElement} canvas
58 | * @return {WebGLRenderingContext}
59 | */
60 | export function getWGLContext(canvas) {
61 | if (!WGL_SUPPORTED) {
62 | throw new Error("Your browser doesn't support WebGL.");
63 | }
64 | const opts = {
65 | alpha: false,
66 | antialias: false,
67 | premultipliedAlpha: false,
68 | stencil: false,
69 | preserveDrawingBuffer: false
70 | };
71 | return (
72 | canvas.getContext("webgl", opts) ||
73 | canvas.getContext("experimental-webgl", opts)
74 | );
75 | };
76 |
--------------------------------------------------------------------------------