karl/view/src/components/Editor/Painter.svelte

369 lines
8.5 KiB
Svelte

<script lang="ts">
import { bufToImageUrl } from "helpers";
import { images as imageStore } from "stores";
import OrbView from "./OrbView";
import { onMount } from "svelte";
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 && orb) orb.setTool(activeTool);
$: 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;
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();
orb.updateOverlay();
}
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";
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 === "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;
// KarlKilian Formel
debugValue = (2 * Math.sqrt(y * (image.height - y))) / image.height;
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 === "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() {
const box = wrapper.getBoundingClientRect();
topLeftX = Math.floor(box.x);
topLeftY = Math.floor(box.y);
wrapperHeightRatio = image.height / box.height;
wrapperWidth = box.width;
}
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();
orb = OrbView(image, cx1, canvas3D);
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>
{#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;
}
#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>