feat(ui): implement fill tool

This commit is contained in:
max_richter 2021-03-16 15:52:41 +01:00
parent b96f820971
commit 312d15a296
15 changed files with 320 additions and 45 deletions

View File

@ -19,6 +19,10 @@
} }
let layerOpacity = 50; let layerOpacity = 50;
let _layerOpacity = localStorage.getItem("layerOpacity");
if (_layerOpacity) {
layerOpacity = _layerOpacity;
}
</script> </script>
<div class={`wrapper tool-${activeTool}`}> <div class={`wrapper tool-${activeTool}`}>

View File

@ -1,5 +1,11 @@
<script lang="ts"> <script lang="ts">
import { AI, bufToImageUrl } from "helpers"; import {
AI,
bufToImageUrl,
createFloodMap,
hexToRGB,
imageToArray,
} from "helpers";
import { images as imageStore } from "stores"; import { images as imageStore } from "stores";
import OrbView from "./OrbView"; import OrbView from "./OrbView";
import { onMount } from "svelte"; import { onMount } from "svelte";
@ -53,6 +59,7 @@
let topLeftY = 0; let topLeftY = 0;
let wrapperHeightRatio = 1; let wrapperHeightRatio = 1;
let wrapperWidth = 0; let wrapperWidth = 0;
const pixelAmount = image.width * image.height;
let xOffset = 0; let xOffset = 0;
@ -160,6 +167,56 @@
prePolygonImage.src = canvas.toDataURL(); prePolygonImage.src = canvas.toDataURL();
} }
let preFloodMx, floodMap, preFloodImage: ArrayBuffer, floodImage: ImageData;
function handleFloodFill() {
if (!floodMap) return;
const amount = 255 - Math.floor(Math.abs(mx - preFloodMx) / 4);
const [r, g, b] = hexToRGB(activeColor);
for (let i = 0; i < pixelAmount; i++) {
if (floodMap[i] > amount) {
floodImage.data[i * 4 + 0] = r;
floodImage.data[i * 4 + 1] = g;
floodImage.data[i * 4 + 2] = b;
floodImage.data[i * 4 + 3] = 255;
} else {
floodImage.data[i * 4 + 0] = preFloodImage[i * 4 + 0];
floodImage.data[i * 4 + 1] = preFloodImage[i * 4 + 1];
floodImage.data[i * 4 + 2] = preFloodImage[i * 4 + 2];
floodImage.data[i * 4 + 3] = preFloodImage[i * 4 + 3];
}
}
cx1.putImageData(floodImage, 0, 0);
}
async function savePreFlood() {
preFloodMx = mx;
prePolygonImage.src = canvas.toDataURL();
floodImage = cx1.getImageData(0, 0, image.width, image.height);
preFloodImage = cx1
.getImageData(0, 0, image.width, image.height)
.data.slice(0);
const x = Math.floor(
mx * wrapperHeightRatio - xOffset * wrapperHeightRatio
);
const y = Math.floor(my * wrapperHeightRatio);
floodMap = await createFloodMap(image, x, y);
// const pixelAmount = image.width * image.height;
// const withAlpha = new Uint8ClampedArray(pixelAmount * 4);
// for (let i = 0; i < pixelAmount; i++) {
// withAlpha[i * 4 + 0] = floodMap[i];
// withAlpha[i * 4 + 1] = floodMap[i];
// withAlpha[i * 4 + 2] = floodMap[i];
// withAlpha[i * 4 + 3] = 255;
// }
// cx1.putImageData(new ImageData(withAlpha, image.width, image.height), 0, 0);
}
function handlePolygonMouseDown() { function handlePolygonMouseDown() {
if (polygonPoints.length < 2) savePrePolygon(); if (polygonPoints.length < 2) savePrePolygon();
@ -242,6 +299,9 @@
if (activeTool === "smooth_polygon") { if (activeTool === "smooth_polygon") {
savePrePolygon(); savePrePolygon();
} }
if (activeTool === "flood_fill") {
savePreFlood();
}
if (activeTool === "polygon") { if (activeTool === "polygon") {
if (isDbClick) { if (isDbClick) {
handleFinishPolygon(); handleFinishPolygon();
@ -274,6 +334,10 @@
(downOffset + e.clientX - downX) % (image.width / wrapperHeightRatio); (downOffset + e.clientX - downX) % (image.width / wrapperHeightRatio);
} }
if (activeTool === "flood_fill") {
handleFloodFill();
}
if (activeTool === "erasor") { if (activeTool === "erasor") {
cx1.globalCompositeOperation = "destination-out"; cx1.globalCompositeOperation = "destination-out";
cx1.fillStyle = "#" + activeColor; cx1.fillStyle = "#" + activeColor;

View File

@ -6,7 +6,14 @@
export let activeTool = "pan"; export let activeTool = "pan";
const tools = ["pan", "brush", "erasor", "smooth_polygon", "polygon"]; const tools = [
"pan",
"brush",
"erasor",
"smooth_polygon",
"polygon",
"flood_fill",
];
$: if (activeTool) { $: if (activeTool) {
if (!tools.includes(activeTool)) activeTool = "brush"; if (!tools.includes(activeTool)) activeTool = "brush";
@ -41,11 +48,13 @@
<style> <style>
button.active { button.active {
background-color: black; background-color: black;
color: white;
} }
button { button {
width: 50px; width: 50px;
height: 50px; height: 50px;
color: black;
background-color: transparent; background-color: transparent;
margin-left: 5px; margin-left: 5px;
margin-bottom: 5px; margin-bottom: 5px;

View File

@ -5,7 +5,7 @@
const minRadius = 1; const minRadius = 1;
const maxRadius = 100; const maxRadius = 100;
export let brushRadius = 10; export let brushRadius = 10;
export const layerOpacity = 50; export let layerOpacity = 50;
const colors = ["ff0000", "00ff00", "0000ff", "ffff00", "00ffff", "ff00ff"]; const colors = ["ff0000", "00ff00", "0000ff", "ffff00", "00ffff", "ff00ff"];
@ -14,6 +14,10 @@
localStorage.setItem("activeColor", activeColor); localStorage.setItem("activeColor", activeColor);
} }
$: if (layerOpacity) {
localStorage.setItem("layerOpacity", layerOpacity);
}
function handleMouseWheel(e) { function handleMouseWheel(e) {
brushRadius = Math.min( brushRadius = Math.min(
Math.max(brushRadius - e.deltaY / 10, minRadius), Math.max(brushRadius - e.deltaY / 10, minRadius),
@ -47,15 +51,15 @@
bind:value={brushRadius} bind:value={brushRadius}
/> />
{/if} {/if}
<!-- <label for="brush-radius">LayerOpacity</label> <label for="layer-opacity">LayerOpacity</label>
<input <input
id="brush-radius" id="layer-opacity"
type="range" type="range"
min="0" min="0"
max="100" max="100"
steps="1" steps="1"
bind:value={layerOpacity} bind:value={layerOpacity}
/> --> />
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { bufToImageUrl } from "."; import { bufToImageUrl, imageToArray } from ".";
const worker = new Worker("build/workers/ai-worker.js"); const worker = new Worker("build/workers/ai-worker.js");
@ -14,26 +14,13 @@ worker.addEventListener("message", ev => {
} }
}) })
const analyze = (img: Image) => new Promise((res, rej) => { const analyze = (img: Image) => new Promise(async (res, rej) => {
i++; i++;
const _i = i; const _i = i;
const canvas = document.createElement("canvas"); const pixels = await imageToArray(img);
canvas.width = img.width;
canvas.height = img.height;
const cx = canvas.getContext("2d");
const image = document.createElement("img");
image.onload = () => {
cx.drawImage(image, 0, 0);
const pixels = cx.getImageData(0, 0, image.width, image.height).data
worker.postMessage({ i: _i, pixels, width: img.width, height: img.height }); worker.postMessage({ i: _i, pixels, width: img.width, height: img.height });
}
image.src = bufToImageUrl(img.data, img.type);
cb[_i] = res; cb[_i] = res;
}); });

View File

@ -1,16 +1,4 @@
import { images } from "../stores"; import _pixelWorker from "./_pixelWorker";
const worker = new Worker("build/workers/pixel-worker.js");
let i = 0;
let cb = {};
worker.addEventListener("message", ev => {
if (ev.data.i in cb) {
cb[ev.data.i](ev.data.result);
}
})
interface res { interface res {
color: string color: string
@ -19,9 +7,6 @@ interface res {
value: number value: number
} }
export default (img: Image, correctDistortion: boolean): Promise<res[]> => new Promise((res, rej) => { export default async (img: Image, correctDistortion: boolean): Promise<res[]> => {
i++; return await _pixelWorker({ pixels: img.overlayData, width: img.width, height: img.height, correctDistortion, type: "count" })
const _i = i; };
worker.postMessage({ i: _i, pixels: img.overlayData, width: img.width, height: img.height, correctDistortion });
cb[_i] = res;
});

View File

@ -0,0 +1,6 @@
import hexToRgb from "./hexToRGB"
import _pixelWorker from "./_pixelWorker";
export default async (floodMap: Uint8ClampedArray, image: Uint8ClampedArray, color: string, amount: number) => {
return await _pixelWorker({ type: "flood_fill", floodMap, image, color, amount });
}

View File

@ -0,0 +1,6 @@
import { imageToArray } from "helpers";
import _pixelWorker from "./_pixelWorker"
export default async (image: Image, x: number, y: number): Promise<Uint8ClampedArray> => {
return await _pixelWorker({ type: "flood_map", pixels: await imageToArray(image, false), x, y, width: image.width, height: image.height });
}

View File

@ -0,0 +1,34 @@
import BuffToImg from "./BuffToImg";
export default (img: Image, withAlpha = true) => new Promise((resolve) => {
const c = document.createElement("canvas");
c.width = img.width;
c.height = img.height;
const ctx = c.getContext("2d");
const imgEl = new Image(img.width, img.height);
imgEl.src = BuffToImg(img.data, img.type)
imgEl.onload = () => {
ctx.drawImage(imgEl, 0, 0);
const alphaPixels = ctx.getImageData(0, 0, img.width, img.height).data;
if (withAlpha) return resolve(alphaPixels);
const pixelAmount = img.width * img.height;
const pixels = new Uint8ClampedArray(pixelAmount * 3);
for (let i = 0; i < pixelAmount; i++) {
pixels[i * 3 + 0] = alphaPixels[i * 4 + 0];
pixels[i * 3 + 1] = alphaPixels[i * 4 + 1];
pixels[i * 3 + 2] = alphaPixels[i * 4 + 2];
}
resolve(pixels);
}
});

View File

@ -0,0 +1,18 @@
const worker = new Worker("build/workers/pixel-worker.js");
let i = 0;
let cb = {};
worker.addEventListener("message", ev => {
if (ev.data.i in cb) {
cb[ev.data.i](ev.data.result);
}
})
export default (msg: any): Promise<any> => new Promise((res, rej) => {
i++;
const _i = i;
worker.postMessage({ i: _i, ...msg });
cb[_i] = res;
});

View File

@ -0,0 +1,8 @@
export default function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
] : null;
}

View File

@ -2,4 +2,7 @@ export { default as bufToImageUrl } from "./BuffToImg";
export { default as fileToImage } from "./FileToImage"; export { default as fileToImage } from "./FileToImage";
export { default as countPixels } from "./CountPixels"; export { default as countPixels } from "./CountPixels";
export { default as downloadImage } from "./DownloadImage"; export { default as downloadImage } from "./DownloadImage";
export { default as imageToArray } from "./ImageToArray";
export { default as createFloodMap } from "./FloodFillMap";
export { default as hexToRGB } from "./hexToRGB";
export { default as AI } from "./AI"; export { default as AI } from "./AI";

View File

@ -0,0 +1,37 @@
<script>
export let color = "black";
</script>
<svg
width="100"
height="100"
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M88.0216 51.8241L45.4864 94.3593L11.9785 60.8514L54.5137 18.3162L88.0216 51.8241Z"
stroke={color}
stroke-width="5"
/>
<path
d="M68.5713 64.4131L22.767 64.4129L45.7679 87.2165L68.5713 64.4131Z"
fill={color}
/>
<path d="M62.069 45.4335L66.4193 5.31409" stroke={color} stroke-width="5" />
<circle
cx="62.069"
cy="44.7038"
r="6.92501"
transform="rotate(45 62.069 44.7038)"
stroke={color}
stroke-width="5"
/>
</svg>
<style>
svg {
width: 100%;
height: 100%;
}
</style>

View File

@ -9,6 +9,7 @@ export { default as brush } from "./Brush.svelte"
export { default as clear } from "./Clear.svelte" export { default as clear } from "./Clear.svelte"
export { default as cross } from "./Cross.svelte" export { default as cross } from "./Cross.svelte"
export { default as erasor } from "./Erasor.svelte" export { default as erasor } from "./Erasor.svelte"
export { default as flood_fill } from "./Fill.svelte"
export { default as pan } from "./Pan.svelte" export { default as pan } from "./Pan.svelte"
export { default as polygon } from "./Polygon.svelte" export { default as polygon } from "./Polygon.svelte"
export { default as smooth_polygon } from "./SmoothPolygon.svelte" export { default as smooth_polygon } from "./SmoothPolygon.svelte"

View File

@ -1,10 +1,11 @@
import hexToRGB from "helpers/hexToRGB";
export { }; export { };
self.addEventListener('message', function (e) { const countColors = ({ pixels, width, height }) => {
const { data: { i, pixels, correctDistortion = true, width, height } } = e;
// This is the old method just by counting pixels // This is the old method just by counting pixels
const oldStore = {}; const oldStore = {};
@ -80,6 +81,114 @@ self.addEventListener('message', function (e) {
} }
}).sort((a, b) => a.value > b.value ? -1 : 1) }).sort((a, b) => a.value > b.value ? -1 : 1)
return result;
}
const distance2D = (x1, y1, x2, y2) => {
const dx = x1 - x2;
const dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
const distance3D = (x1, y1, z1, x2, y2, z2) => {
const dx = x1 - x2;
const dy = y1 - y2;
const dz = z1 - z2;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
const createFloodFillMap = ({ pixels, x, y, width, height }: { pixels: Uint8ClampedArray, x: number, y: number, width: number, height: number }) => {
const pixelAmount = width * height;
const index = (y * width + x) * 3;
const _R = pixels[index + 0];
const _G = pixels[index + 1];
const _B = pixels[index + 2];
let distanceWeight = 0.7;
// This is the distance to the pixel that is furthest away from xy
const maxDistance = distance2D((x < (width / 2)) ? width : 0, (y < (height / 2)) ? height : 0, x, y);
console.log(maxDistance)
// This the distance to the color that is furthest away from _R_G_B;
let maxColorDistance = 0;
for (let i = 0; i < pixelAmount; i++) {
const r = pixels[i * 3 + 0];
const g = pixels[i * 3 + 1];
const b = pixels[i * 3 + 2];
const dist = distance3D(r, g, b, _R, _G, _B);
if (dist > maxColorDistance) maxColorDistance = dist;
}
const floodFillMap = new Uint8ClampedArray(pixelAmount);
const maxWeight = (maxDistance * distanceWeight) + maxColorDistance * (1 - distanceWeight);
for (let i = 0; i < pixelAmount; i++) {
const _x = i % width;
const _y = Math.floor(i / width);
const r = pixels[i * 3 + 0];
const g = pixels[i * 3 + 1];
const b = pixels[i * 3 + 2];
let dist = distance2D(_x, _y, x, y);
let colDist = distance3D(r, g, b, _R, _G, _B);
let weight = 1 - ((dist * distanceWeight) + (colDist * (1 - distanceWeight))) / maxWeight;
floodFillMap[i] = Math.floor(weight * 255);
}
return floodFillMap;
}
const drawFloodFill = ({ floodMap, image, color, amount }: { floodMap: Uint8ClampedArray, image: Uint8ClampedArray, color: string, amount: number }) => {
const [r, g, b] = hexToRGB(color);
return image;
}
const send = (result, i) => {
//@ts-ignore //@ts-ignore
self.postMessage({ result, i }); self.postMessage({ result, i });
}
self.addEventListener('message', function (e) {
const data = e.data;
const { i, type = "count" } = data;
switch (type) {
case "count":
send(countColors(data), i);
break;
case "flood_map":
send(createFloodFillMap(data), i);
break;
case "flood_fill":
send(drawFloodFill(data), i);
break;
default:
console.error("ERROR IN POXEL WORKER")
}
}, false); }, false);