460 lines
10 KiB
Svelte
460 lines
10 KiB
Svelte
<script lang="ts">
|
|
import { AI, bufToImageUrl } from "helpers";
|
|
import { images as imageStore } from "stores";
|
|
import OrbView from "./OrbView";
|
|
import { onMount } from "svelte";
|
|
import Toast from "components/Toast";
|
|
|
|
export let image: Image;
|
|
export let activeTool = "pan";
|
|
export let brushRadius = 20;
|
|
export let activeColor = "ff0000";
|
|
export let layerOpacity = 50;
|
|
|
|
const imageUrl = bufToImageUrl(image.data, image.type);
|
|
|
|
let canvas: HTMLCanvasElement;
|
|
let canvas2: HTMLCanvasElement;
|
|
let canvas3D: HTMLCanvasElement;
|
|
let wrapper: HTMLDivElement;
|
|
let cx1: CanvasRenderingContext2D;
|
|
let cx2: CanvasRenderingContext2D;
|
|
|
|
let orb: ReturnType<typeof OrbView>;
|
|
$: if (activeTool) {
|
|
if (orb) {
|
|
orb.setTool(activeTool);
|
|
}
|
|
if (activeTool === "clear") {
|
|
cx1.clearRect(0, 0, image.width, image.height);
|
|
cx2.clearRect(0, 0, image.width, image.height);
|
|
saveToImage();
|
|
activeTool = "brush";
|
|
}
|
|
}
|
|
$: if (layerOpacity && orb) orb.setOpacity(layerOpacity);
|
|
|
|
let mode = "2d";
|
|
let isOriginal = true;
|
|
|
|
let topLeftX = 0;
|
|
let topLeftY = 0;
|
|
let wrapperHeightRatio = 1;
|
|
let wrapperWidth = 0;
|
|
|
|
let xOffset = 0;
|
|
|
|
$: if (xOffset !== undefined) {
|
|
localStorage.setItem("xOffset", "" + xOffset);
|
|
}
|
|
if ("xOffset" in localStorage) {
|
|
xOffset = parseInt(localStorage.getItem("xOffset"));
|
|
}
|
|
|
|
let isDown = false;
|
|
let mx = 0;
|
|
let my = 0;
|
|
let downX, downOffset;
|
|
|
|
let debugValue = 0;
|
|
let debugY = 0;
|
|
|
|
let isStrPressed = false;
|
|
let isSpacePressed = false;
|
|
let lastActiveTool;
|
|
|
|
function scaleImageData(imageData, scale) {
|
|
var newCanvas = document.createElement("canvas");
|
|
newCanvas.width = imageData.width;
|
|
newCanvas.height = imageData.height;
|
|
|
|
newCanvas.getContext("2d").putImageData(imageData, 0, 0);
|
|
|
|
// Second canvas, for scaling
|
|
var scaleCanvas = document.createElement("canvas");
|
|
scaleCanvas.width = imageData.width * scale;
|
|
scaleCanvas.height = imageData.height * scale;
|
|
|
|
var scaleCtx = scaleCanvas.getContext("2d");
|
|
|
|
scaleCtx.scale(scale, scale);
|
|
scaleCtx.drawImage(newCanvas, 0, 0);
|
|
|
|
var scaledImageData = scaleCtx.getImageData(
|
|
0,
|
|
0,
|
|
scaleCanvas.width * scale,
|
|
scaleCanvas.height * scale
|
|
);
|
|
|
|
return scaledImageData;
|
|
}
|
|
|
|
const saveToImage = () => {
|
|
const imageData = cx1.getImageData(0, 0, image.width, image.height);
|
|
image.overlayData = imageData.data;
|
|
imageStore.updateImage(image);
|
|
cx2.putImageData(imageData, 0, 0);
|
|
};
|
|
|
|
function drawBrush() {
|
|
const cx = isOriginal ? cx1 : cx2;
|
|
cx.fillStyle = "#" + activeColor;
|
|
cx.beginPath();
|
|
|
|
const x =
|
|
mx * wrapperHeightRatio -
|
|
xOffset * wrapperHeightRatio +
|
|
(isOriginal ? 0 : image.width * wrapperHeightRatio);
|
|
const y = my * wrapperHeightRatio;
|
|
|
|
cx.arc(x, y, brushRadius * wrapperHeightRatio, 0, 2 * Math.PI);
|
|
cx.fill();
|
|
cx.closePath();
|
|
}
|
|
|
|
let polygonPoints = [];
|
|
let prePolygonImage = new Image(image.width, image.height);
|
|
let lastPolyX;
|
|
let lastPolyY;
|
|
function drawPolygon() {
|
|
const x = Math.floor(
|
|
mx * wrapperHeightRatio - xOffset * wrapperHeightRatio
|
|
);
|
|
const y = Math.floor(my * wrapperHeightRatio);
|
|
|
|
if (Math.abs(lastPolyX - x) + Math.abs(lastPolyY - y) < 2) return;
|
|
|
|
cx1.clearRect(0, 0, image.width, image.height);
|
|
cx1.drawImage(prePolygonImage, 0, 0, image.width, image.height);
|
|
|
|
polygonPoints.push(x, y);
|
|
|
|
cx1.beginPath();
|
|
cx1.moveTo(polygonPoints[0], polygonPoints[1]);
|
|
for (let i = 2; i < polygonPoints.length; i += 2) {
|
|
cx1.lineTo(polygonPoints[i], polygonPoints[i + 1]);
|
|
}
|
|
cx1.fillStyle = "#" + activeColor;
|
|
cx1.closePath();
|
|
cx1.fill();
|
|
|
|
lastPolyX = x;
|
|
lastPolyY = y;
|
|
}
|
|
|
|
function savePrePolygon() {
|
|
lastPolyX = undefined;
|
|
lastPolyY = undefined;
|
|
polygonPoints = [];
|
|
prePolygonImage.src = canvas.toDataURL();
|
|
}
|
|
|
|
function switchMode(e: MouseEvent) {
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
|
|
mode = mode === "2d" ? "3d" : "2d";
|
|
|
|
if (mode === "3d") {
|
|
if (!orb) orb = OrbView(image, cx1, canvas3D);
|
|
orb.updateOverlay();
|
|
}
|
|
|
|
mode === "2d" ? orb.stop() : orb.start();
|
|
}
|
|
|
|
function handleMouseDown(e: MouseEvent) {
|
|
if (e.button === 1) {
|
|
lastActiveTool = activeTool;
|
|
activeTool = "pan";
|
|
}
|
|
|
|
downX = e.clientX;
|
|
downOffset = xOffset;
|
|
isDown = true;
|
|
if (activeTool === "brush") drawBrush();
|
|
if (activeTool === "smooth_polygon") {
|
|
savePrePolygon();
|
|
}
|
|
}
|
|
|
|
function handleMouseUp(e) {
|
|
isDown = false;
|
|
if (lastActiveTool) {
|
|
activeTool = lastActiveTool;
|
|
lastActiveTool = undefined;
|
|
}
|
|
|
|
saveToImage();
|
|
}
|
|
|
|
function handleMouseMove(e) {
|
|
mx = Math.floor(e.clientX - topLeftX);
|
|
my = Math.floor(e.clientY - topLeftY);
|
|
|
|
isOriginal = e.target.id === "cx1";
|
|
|
|
//Caclulate y position of pixel
|
|
const y = Math.floor(my * wrapperHeightRatio);
|
|
debugY = y;
|
|
const height = image.height;
|
|
|
|
// KarlKilian Formel
|
|
//debugValue = (2 * Math.sqrt(y * (image.height - y))) / image.height;
|
|
|
|
// New new formel
|
|
debugValue = Math.cos(
|
|
(((360 / height ** 2) * y ** 2 + (-360 / height) * y + 90) / 360) *
|
|
2 *
|
|
Math.PI
|
|
);
|
|
|
|
if (isDown) {
|
|
if (activeTool === "pan") {
|
|
// TODO fix overflowiung
|
|
xOffset =
|
|
(downOffset + e.clientX - downX) % (image.width / wrapperHeightRatio);
|
|
}
|
|
|
|
if (activeTool === "erasor") {
|
|
cx1.globalCompositeOperation = "destination-out";
|
|
cx1.fillStyle = "#" + activeColor;
|
|
cx1.beginPath();
|
|
const mx = Math.floor(
|
|
e.clientX * wrapperHeightRatio -
|
|
topLeftX * wrapperHeightRatio -
|
|
xOffset * wrapperHeightRatio
|
|
);
|
|
const my = Math.floor(
|
|
e.clientY * wrapperHeightRatio - topLeftY * wrapperHeightRatio
|
|
);
|
|
cx1.arc(mx, my, brushRadius * wrapperHeightRatio, 0, 2 * Math.PI);
|
|
cx1.fill();
|
|
cx1.closePath();
|
|
} else {
|
|
cx1.globalCompositeOperation = "source-over";
|
|
}
|
|
|
|
if (activeTool === "smooth_polygon") drawPolygon();
|
|
|
|
if (activeTool === "brush") drawBrush();
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(e) {
|
|
if (e.keyCode === 69) activeTool = "erasor";
|
|
if (e.keyCode === 66) activeTool = "brush";
|
|
if (e.keyCode === 17) isStrPressed = true;
|
|
//SPACE
|
|
if (e.keyCode === 32) {
|
|
isSpacePressed = true;
|
|
if (!lastActiveTool) {
|
|
lastActiveTool = activeTool;
|
|
activeTool = "pan";
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleKeyUp(e) {
|
|
if (e.keyCode === 17) isStrPressed = false;
|
|
//SPACE
|
|
if (e.keyCode === 32) {
|
|
activeTool = lastActiveTool;
|
|
lastActiveTool = undefined;
|
|
}
|
|
}
|
|
|
|
function handleResize() {
|
|
if (!wrapper) return;
|
|
const box = wrapper.getBoundingClientRect();
|
|
topLeftX = Math.floor(box.x);
|
|
topLeftY = Math.floor(box.y);
|
|
wrapperHeightRatio = image.height / box.height;
|
|
wrapperWidth = box.width;
|
|
}
|
|
|
|
let aiState = "AI";
|
|
async function handleAi() {
|
|
if (aiState !== "AI") return;
|
|
|
|
if (
|
|
!(await Toast.confirm(
|
|
"For now this action will overwrite existing mask, continue?"
|
|
))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
aiState = "loading...";
|
|
const res: any = await AI.analyze(image);
|
|
aiState = "Finished";
|
|
setTimeout(() => {
|
|
aiState = "AI";
|
|
}, 1000);
|
|
|
|
console.log(res);
|
|
|
|
var iData = new ImageData(res.segmentationMap, res.width, res.height);
|
|
|
|
const scale = image.width / res.width;
|
|
const s = scaleImageData(iData, scale);
|
|
|
|
cx1.putImageData(s, 0, 0);
|
|
}
|
|
|
|
onMount(() => {
|
|
canvas.width = image.width;
|
|
canvas.height = image.height;
|
|
|
|
canvas2.width = image.width;
|
|
canvas2.height = image.height;
|
|
|
|
cx1 = canvas.getContext("2d");
|
|
cx2 = canvas2.getContext("2d");
|
|
|
|
if (image.overlayData && image.overlayData.byteLength) {
|
|
const imageData = new ImageData(
|
|
new Uint8ClampedArray(image.overlayData),
|
|
image.width,
|
|
image.height
|
|
);
|
|
cx1.putImageData(imageData, 0, 0);
|
|
cx2.putImageData(imageData, 0, 0);
|
|
}
|
|
|
|
handleResize();
|
|
|
|
setTimeout(() => handleResize(), 500);
|
|
setTimeout(() => handleResize(), 2000);
|
|
});
|
|
</script>
|
|
|
|
<svelte:window
|
|
on:keydown={handleKeyDown}
|
|
on:keyup={handleKeyUp}
|
|
on:resize={handleResize}
|
|
/>
|
|
|
|
<div
|
|
on:mousedown={handleMouseDown}
|
|
on:mouseup={handleMouseUp}
|
|
on:mouseleave={handleMouseUp}
|
|
on:mousemove={handleMouseMove}
|
|
bind:this={wrapper}
|
|
class={`wrapper tool-${activeTool} mode-${mode}`}
|
|
class:is-down={isDown}
|
|
style={`background-image: url(${imageUrl}); background-position: ${xOffset}px ${0}px`}
|
|
>
|
|
<button id="mode" on:click={switchMode}>
|
|
{mode}
|
|
</button>
|
|
|
|
<button id="ai" on:click={handleAi}>
|
|
{aiState}
|
|
</button>
|
|
|
|
{#if activeTool === "brush" || activeTool === "erasor"}
|
|
<div
|
|
id="cursor"
|
|
style={`width: ${brushRadius * 2}px; height: ${
|
|
brushRadius * 2
|
|
}px; background-color: #${activeColor}; top: ${my}px; left: ${mx}px`}
|
|
/>
|
|
{/if}
|
|
|
|
<canvas
|
|
id="cx1"
|
|
bind:this={canvas}
|
|
class:visible={mode === "2d"}
|
|
style={`transform: translateX(${xOffset}px); opacity: ${
|
|
layerOpacity / 100
|
|
};`}
|
|
/>
|
|
|
|
<canvas
|
|
id="cx2"
|
|
bind:this={canvas2}
|
|
class:visible={mode === "2d"}
|
|
style={`transform: translateX(calc(${xOffset}px - ${
|
|
xOffset > 0 ? 100 : -100
|
|
}%)); opacity: ${(layerOpacity / 100) * 0.5};`}
|
|
/>
|
|
|
|
<p>h:{image.height} | y:{debugY} | value:{debugValue}</p>
|
|
|
|
<canvas class:visible={mode === "3d"} bind:this={canvas3D} />
|
|
</div>
|
|
|
|
<style>
|
|
p {
|
|
position: absolute;
|
|
top: 0px;
|
|
left: 0px;
|
|
z-index: 1001;
|
|
}
|
|
|
|
#ai {
|
|
position: absolute;
|
|
top: 60px;
|
|
right: 10px;
|
|
z-index: 1001;
|
|
cursor: pointer;
|
|
pointer-events: all;
|
|
}
|
|
|
|
#mode {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
z-index: 1001;
|
|
cursor: pointer;
|
|
pointer-events: all;
|
|
}
|
|
|
|
.tool-erasor > #cursor {
|
|
background-color: transparent !important;
|
|
border: solid medium black;
|
|
}
|
|
|
|
#cursor {
|
|
position: absolute;
|
|
pointer-events: none;
|
|
opacity: 0.5;
|
|
z-index: 99;
|
|
border-radius: 100%;
|
|
transform: translateX(-50%) translateY(-50%);
|
|
}
|
|
|
|
.wrapper.tool-pan {
|
|
cursor: grab;
|
|
}
|
|
|
|
.wrapper.tool-pan.is-down {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.wrapper.mode-3d {
|
|
background-image: none !important;
|
|
}
|
|
|
|
.wrapper {
|
|
position: relative;
|
|
user-select: none;
|
|
overflow: hidden;
|
|
|
|
height: 100%;
|
|
background-size: auto 100%;
|
|
}
|
|
canvas {
|
|
position: absolute;
|
|
display: none;
|
|
height: 100%;
|
|
}
|
|
|
|
canvas.visible {
|
|
display: block;
|
|
}
|
|
</style>
|