second day

This commit is contained in:
max_richter 2021-03-10 01:20:22 +01:00
parent 7fc8feb0cc
commit 521e2a4eb1
36 changed files with 3187 additions and 111 deletions

View File

@ -8,4 +8,8 @@ dev-server:
cd server && gin run main.go
dev-view:
cd view && npm run dev
build: build-view
build-view:
cd view && npm run build

1238
view/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"validate": "svelte-check"
},
"devDependencies": {
"@rollup/plugin-alias": "^3.1.2",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.0.0",
@ -16,6 +17,8 @@
"idb": "^6.0.0",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-glslify": "^1.2.0",
"rollup-plugin-includepaths": "^0.2.4",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
@ -26,6 +29,7 @@
"typescript": "^4.0.0"
},
"dependencies": {
"ogl": "^0.0.65",
"sirv-cli": "^1.0.0",
"svelte-file-dropzone": "^0.0.15"
}

View File

@ -7,14 +7,14 @@
<title>Karls Analyzer</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<link rel='icon' type='image/png' href='favicon.png'>
<link rel='stylesheet' href='global.css'>
<link rel='stylesheet' href='build/bundle.css'>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;900&display=swap" rel="stylesheet">
<script defer src='/build/bundle.js'></script>
<script defer src='build/bundle.js'></script>
</head>
<body>

29
view/public/worker.js Normal file
View File

@ -0,0 +1,29 @@
self.addEventListener('message', function (e) {
const { data: { i, arr: pixels } } = e;
let store = {};
let total = pixels.length / 4;
const threshold = 200;
for (let i = 0; i < total; i++) {
const r = pixels[i * 4 + 0] > threshold ? 1 : 0;
const g = pixels[i * 4 + 1] > threshold ? 1 : 0;
const b = pixels[i * 4 + 2] > threshold ? 1 : 0;
const id = r + "-" + g + "-" + b;
store[id] = store[id] + 1 || 1;
}
Object.keys(store).forEach(k => {
store[k] = store[k] / total;
});
self.postMessage({ res: store, i });
}, false);

View File

@ -5,7 +5,8 @@ import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';
import css from 'rollup-plugin-css-only';
import includePaths from 'rollup-plugin-includepaths';
import glslify from 'rollup-plugin-glslify';
const production = !process.env.ROLLUP_WATCH;
export default {
@ -17,6 +18,12 @@ export default {
file: 'public/build/bundle.js'
},
plugins: [
includePaths({
paths: ["src"],
extensions: [".ts", ".svelte"]
}),
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
compilerOptions: {
@ -28,6 +35,8 @@ export default {
// a separate file - better for performance
css({ output: 'bundle.css' }),
glslify(),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -

View File

@ -1,25 +1,14 @@
<script lang="ts">
import Editor from "./routes/editor.svelte";
import * as routes from "./routes";
import ToastWrapper from "./components/Toast/ToastWrapper.svelte";
import { route as currentRoute } from "./stores";
</script>
<main>
{#if $currentRoute in routes}
<svelte:component this={routes[$currentRoute]} />
{/if}
{#if $currentRoute.startsWith("editor")}
<Editor />
{:else if $currentRoute in routes}
<svelte:component this={routes[$currentRoute]} />
{/if}
<ToastWrapper />
</main>
<style>
main {
height: 100%;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>
<ToastWrapper />

View File

@ -0,0 +1,78 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { countPixels } from "../helpers";
export let img: Image;
import { quartInOut } from "svelte/easing";
let visible = true;
function scaleX(node, { duration, delay }) {
return {
delay,
duration,
css: (t) => {
const eased = quartInOut(t);
return `
transform: scaleX(${eased});`;
},
};
}
const prom = countPixels(img.overlayData)
.then((res) => {
return Object.keys(res).map((c) => {
const [r, g, b] = c.split("-").map((n) => parseInt(n) * 255);
return {
r,
g,
b,
amount: res[c],
};
});
})
.then((colors) => colors.sort((a, b) => (a.amount > b.amount ? -1 : 1)));
</script>
<h3>ANALYZER</h3>
{#await prom}
<p>Loading...</p>
{:then result}
<div class="list">
{#each result as color, i}
<p>
{Math.floor(color.amount * 1000) / 10}%
</p>
<div
transition:scaleX={{ duration: 500, delay: i * 200 }}
class="bar"
style={`width: ${color.amount * 100}%; background-color: rgb(${
color.r
}, ${color.g}, ${color.b})`}
/>
{/each}
</div>
{/await}
<style>
.list {
display: grid;
grid-template-rows: auto;
grid-template-columns: min-content 1fr;
}
.list > p {
margin: 0;
text-align: right;
padding-right: 10px;
}
.bar {
height: 20px;
margin-bottom: 20px;
transform-origin: 0px 0px;
}
</style>

View File

@ -32,6 +32,7 @@
export let containerClasses = "";
export let containerStyles = "";
export let disableDefaultStyles = false;
export let disableClick = false;
const dispatch = createEventDispatcher();
@ -311,7 +312,7 @@
on:keydown={composeKeyboardHandler(onKeyDownCb)}
on:focus={composeKeyboardHandler(onFocusCb)}
on:blur={composeKeyboardHandler(onBlurCb)}
on:click={composeHandler(onClickCb)}
on:click={!disableClick && composeHandler(onClickCb)}
on:dragenter={composeDragHandler(onDragEnterCb)}
on:dragover={composeDragHandler(onDragOverCb)}
on:dragleave={composeDragHandler(onDragLeaveCb)}
@ -324,27 +325,9 @@
autocomplete="off"
tabindex="-1"
on:change={onDropCb}
on:click={onInputElementClick}
on:click={!disableClick && onInputElementClick}
bind:this={inputRef}
style="display: none;"
/>
<slot>
<p>Drag 'n' drop some files here, or click to select files</p>
</slot>
<slot />
</div>
<style>
.dropzone {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
border-width: 2px;
outline: solid thin black;
width: 300px;
}
.dropzone:focus {
border-color: #2196f3;
}
</style>

View File

@ -0,0 +1,2 @@
<script>
</script>

View File

@ -0,0 +1,331 @@
import { Vec2, Vec3 } from 'ogl';
const STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, DOLLY_PAN: 3 };
const tempVec3 = new Vec3();
const tempVec2a = new Vec2();
const tempVec2b = new Vec2();
export function Orbit(
object,
{
element = document,
enabled = true,
target = new Vec3(),
ease = 0.25,
inertia = 0.85,
enableRotate = true,
rotateSpeed = 0.1,
autoRotate = false,
autoRotateSpeed = 1.0,
enableZoom = true,
zoomSpeed = 1,
enablePan = true,
panSpeed = 0.1,
minPolarAngle = 0,
maxPolarAngle = Math.PI,
minAzimuthAngle = -Infinity,
maxAzimuthAngle = Infinity,
minDistance = 0,
maxDistance = Infinity,
} = {}
) {
this.enabled = enabled;
this.target = target;
// Catch attempts to disable - set to 1 so has no effect
ease = ease || 1;
inertia = inertia || 0;
this.minDistance = minDistance;
this.maxDistance = maxDistance;
// current position in sphericalTarget coordinates
const sphericalDelta = { radius: 1, phi: 0, theta: 0 };
const sphericalTarget = { radius: 1, phi: 0, theta: 0 };
const spherical = { radius: 1, phi: 0, theta: 0 };
const panDelta = new Vec3();
// Grab initial position values
const offset = new Vec3();
offset.copy(object.position).sub(this.target);
spherical.radius = sphericalTarget.radius = offset.distance();
spherical.theta = sphericalTarget.theta = Math.atan2(offset.x, offset.z);
spherical.phi = sphericalTarget.phi = Math.acos(Math.min(Math.max(offset.y / sphericalTarget.radius, -1), 1));
this.offset = offset;
this.update = () => {
if (autoRotate) {
handleAutoRotate();
}
// apply delta
sphericalTarget.radius *= sphericalDelta.radius;
sphericalTarget.theta += sphericalDelta.theta;
sphericalTarget.phi += sphericalDelta.phi;
// apply boundaries
sphericalTarget.theta = Math.max(minAzimuthAngle, Math.min(maxAzimuthAngle, sphericalTarget.theta));
sphericalTarget.phi = Math.max(minPolarAngle, Math.min(maxPolarAngle, sphericalTarget.phi));
sphericalTarget.radius = Math.max(this.minDistance, Math.min(this.maxDistance, sphericalTarget.radius));
// ease values
spherical.phi += (sphericalTarget.phi - spherical.phi) * ease;
spherical.theta += (sphericalTarget.theta - spherical.theta) * ease;
spherical.radius += (sphericalTarget.radius - spherical.radius) * ease;
// apply pan to target. As offset is relative to target, it also shifts
this.target.add(panDelta);
// apply rotation to offset
let sinPhiRadius = spherical.radius * Math.sin(Math.max(0.000001, spherical.phi));
offset.x = sinPhiRadius * Math.sin(spherical.theta);
offset.y = spherical.radius * Math.cos(spherical.phi);
offset.z = sinPhiRadius * Math.cos(spherical.theta);
// Apply updated values to object
object.position.copy(this.target).add(offset);
object.lookAt(this.target);
// Apply inertia to values
sphericalDelta.theta *= inertia;
sphericalDelta.phi *= inertia;
panDelta.multiply(inertia);
// Reset scale every frame to avoid applying scale multiple times
sphericalDelta.radius = 1;
};
// Updates internals with new position
this.forcePosition = () => {
offset.copy(object.position).sub(this.target);
spherical.radius = sphericalTarget.radius = offset.distance();
spherical.theta = sphericalTarget.theta = Math.atan2(offset.x, offset.z);
spherical.phi = sphericalTarget.phi = Math.acos(Math.min(Math.max(offset.y / sphericalTarget.radius, -1), 1));
object.lookAt(this.target);
};
// Everything below here just updates panDelta and sphericalDelta
// Using those two objects' values, the orbit is calculated
const rotateStart = new Vec2();
const panStart = new Vec2();
const dollyStart = new Vec2();
let state = STATE.NONE;
this.mouseButtons = { ORBIT: 0, ZOOM: 0, PAN: 0 };
function getZoomScale() {
return Math.pow(0.95, zoomSpeed);
}
function panLeft(distance, m) {
tempVec3.set(m[0], m[1], m[2]);
tempVec3.multiply(-distance);
panDelta.add(tempVec3);
}
function panUp(distance, m) {
tempVec3.set(m[4], m[5], m[6]);
tempVec3.multiply(distance);
panDelta.add(tempVec3);
}
const pan = (deltaX, deltaY) => {
let el = element === document ? document.body : element;
tempVec3.copy(object.position).sub(this.target);
let targetDistance = tempVec3.distance();
targetDistance *= Math.tan((((object.fov || 45) / 2) * Math.PI) / 180.0);
panLeft((2 * deltaX * targetDistance) / el.clientHeight, object.matrix);
panUp((2 * deltaY * targetDistance) / el.clientHeight, object.matrix);
};
function dolly(dollyScale) {
sphericalDelta.radius /= dollyScale;
}
function handleAutoRotate() {
const angle = ((2 * Math.PI) / 60 / 60) * autoRotateSpeed;
sphericalDelta.theta -= angle;
}
function handleMoveRotate(x, y) {
tempVec2a.set(x, y);
tempVec2b.sub(tempVec2a, rotateStart).multiply(rotateSpeed);
let el = element === document ? document.body : element;
sphericalDelta.theta -= (2 * Math.PI * tempVec2b.x) / el.clientHeight;
sphericalDelta.phi -= (2 * Math.PI * tempVec2b.y) / el.clientHeight;
rotateStart.copy(tempVec2a);
}
function handleMouseMoveDolly(e) {
tempVec2a.set(e.clientX, e.clientY);
tempVec2b.sub(tempVec2a, dollyStart);
if (tempVec2b.y > 0) {
dolly(getZoomScale());
} else if (tempVec2b.y < 0) {
dolly(1 / getZoomScale());
}
dollyStart.copy(tempVec2a);
}
function handleMovePan(x, y) {
tempVec2a.set(x, y);
tempVec2b.sub(tempVec2a, panStart).multiply(panSpeed);
pan(tempVec2b.x, tempVec2b.y);
panStart.copy(tempVec2a);
}
function handleTouchStartDollyPan(e) {
if (enableZoom) {
let dx = e.touches[0].pageX - e.touches[1].pageX;
let dy = e.touches[0].pageY - e.touches[1].pageY;
let distance = Math.sqrt(dx * dx + dy * dy);
dollyStart.set(0, distance);
}
if (enablePan) {
let x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX);
let y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY);
panStart.set(x, y);
}
}
function handleTouchMoveDollyPan(e) {
if (enableZoom) {
let dx = e.touches[0].pageX - e.touches[1].pageX;
let dy = e.touches[0].pageY - e.touches[1].pageY;
let distance = Math.sqrt(dx * dx + dy * dy);
tempVec2a.set(0, distance);
tempVec2b.set(0, Math.pow(tempVec2a.y / dollyStart.y, zoomSpeed));
dolly(tempVec2b.y);
dollyStart.copy(tempVec2a);
}
if (enablePan) {
let x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX);
let y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY);
handleMovePan(x, y);
}
}
const onMouseDown = (e) => {
if (!this.enabled) return;
rotateStart.set(e.clientX, e.clientY);
state = STATE.ROTATE;
if (state !== STATE.NONE) {
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mouseup', onMouseUp, false);
}
};
const onMouseMove = (e) => {
if (!this.enabled) return;
switch (state) {
case STATE.ROTATE:
if (enableRotate === false) return;
handleMoveRotate(e.clientX, e.clientY);
break;
case STATE.DOLLY:
if (enableZoom === false) return;
handleMouseMoveDolly(e);
break;
case STATE.PAN:
if (enablePan === false) return;
handleMovePan(e.clientX, e.clientY);
break;
}
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove, false);
window.removeEventListener('mouseup', onMouseUp, false);
state = STATE.NONE;
};
const onMouseWheel = (e) => {
if (!this.enabled || !enableZoom || (state !== STATE.NONE && state !== STATE.ROTATE)) return;
e.stopPropagation();
e.preventDefault();
if (e.deltaY < 0) {
dolly(1 / getZoomScale());
} else if (e.deltaY > 0) {
dolly(getZoomScale());
}
};
const onTouchStart = (e) => {
if (!this.enabled) return;
e.preventDefault();
switch (e.touches.length) {
case 1:
if (enableRotate === false) return;
rotateStart.set(e.touches[0].pageX, e.touches[0].pageY);
state = STATE.ROTATE;
break;
case 2:
if (enableZoom === false && enablePan === false) return;
handleTouchStartDollyPan(e);
state = STATE.DOLLY_PAN;
break;
default:
state = STATE.NONE;
}
};
const onTouchMove = (e) => {
if (!this.enabled) return;
e.preventDefault();
e.stopPropagation();
switch (e.touches.length) {
case 1:
if (enableRotate === false) return;
handleMoveRotate(e.touches[0].pageX, e.touches[0].pageY);
break;
case 2:
if (enableZoom === false && enablePan === false) return;
handleTouchMoveDollyPan(e);
break;
default:
state = STATE.NONE;
}
};
const onTouchEnd = () => {
if (!this.enabled) return;
state = STATE.NONE;
};
const onContextMenu = (e) => {
if (!this.enabled) return;
e.preventDefault();
};
function addHandlers() {
element.addEventListener('contextmenu', onContextMenu, false);
element.addEventListener('mousedown', onMouseDown, false);
element.addEventListener('wheel', onMouseWheel, { passive: false });
element.addEventListener('touchstart', onTouchStart, { passive: false });
element.addEventListener('touchend', onTouchEnd, false);
element.addEventListener('touchmove', onTouchMove, { passive: false });
}
this.remove = function () {
element.removeEventListener('contextmenu', onContextMenu);
element.removeEventListener('mousedown', onMouseDown);
element.removeEventListener('wheel', onMouseWheel);
element.removeEventListener('touchstart', onTouchStart);
element.removeEventListener('touchend', onTouchEnd);
element.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
addHandlers();
}

View File

@ -0,0 +1,77 @@
<script lang="ts">
import { route as currentRoute } from "stores";
import Painter from "./Painter.svelte";
import TopBar from "./TopBar.svelte";
import ToolBox from "./ToolBox.svelte";
export let image: Image;
let activeTool = "brush";
let brushRadius = 20;
let activeColor = "ff0000";
let layerOpacity = 50;
</script>
<div class={`wrapper tool-${activeTool}`}>
<div class="main">
<Painter
{image}
bind:layerOpacity
bind:activeTool
bind:brushRadius
bind:activeColor
/>
</div>
<div class="footer">footer</div>
<div class="toolbox">
<ToolBox bind:activeTool />
</div>
<div class="top">
<TopBar bind:layerOpacity bind:brushRadius bind:activeColor />
</div>
<div class="back">
<button on:click={() => currentRoute.set("list")}>&lt;</button>
</div>
</div>
<style>
.wrapper {
width: 100vw;
overflow: hidden;
display: grid;
height: 100%;
grid-template-columns: 50px 1fr;
grid-template-rows: 30px 1fr 30px;
gap: 0px 0px;
grid-template-areas:
"back top"
"toolbox main"
"toolbox footer";
}
.main {
width: 100%;
height: 100%;
grid-area: main;
}
.footer {
grid-area: footer;
}
.toolbox {
padding-top: 10px;
grid-area: toolbox;
}
.top {
grid-area: top;
}
.back {
grid-area: back;
}
.back > button {
background-color: white;
border: none;
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,8 @@
precision highp float;
uniform sampler2D tMap;
uniform float opacity;
varying vec2 vUv;
void main() {
gl_FragColor.rgba = texture2D(tMap, vUv).rgba;
gl_FragColor.a *= opacity;
}

View File

@ -0,0 +1,10 @@
attribute vec2 uv;
attribute vec3 position;
attribute vec3 normal;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

View File

@ -0,0 +1,136 @@
import { Renderer, Camera, Transform, Texture, Sphere, Program, Mesh } from "ogl"
import { Orbit } from "./CustomOrbit"
import { bufToImageUrl } from "helpers";
import VertexShader from "./Orb.vert";
import FragmentShader from "./Orb.frag";
import Toast from "../Toast";
let hasConfirmed = false;
export default (image: Image, canvas2D: CanvasRenderingContext2D, canvas3D: HTMLCanvasElement) => {
const renderer = new Renderer({ dpr: 2, canvas: canvas3D });
const gl = renderer.gl;
gl.clearColor(1, 1, 1, 1);
const camera = new Camera(gl, { fov: 45 });
camera.position.set(0, 0, 8);
const controls = new Orbit(camera, {
enablePan: false,
enableZoom: false,
rotateSpeed: -0.1
});
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
}
window.addEventListener('resize', resize, false);
resize();
const scene = new Transform();
// Texture is equirectangular
const texture = new Texture(gl);
const img = new Image();
img.onload = () => (texture.image = img);
img.src = bufToImageUrl(image.data, image.type);
// Use Sphere geometry to render equirectangular textures
const geometry = new Sphere(gl, { radius: 1, widthSegments: 64 });
const program = new Program(gl, {
vertex: VertexShader,
fragment: FragmentShader,
uniforms: {
tMap: { value: texture },
opacity: { value: 1 }
},
// Need inside of sphere to be visible
cullFace: null,
});
// Camera will dwell inside skybox
const skybox = new Mesh(gl, { geometry, program });
skybox.scale.set(10);
skybox.scale.x = -10;
skybox.setParent(scene);
const overlayTexture = new Texture(gl, {
image: new Uint8Array(image.data),
width: image.width,
height: image.height,
});
// Use Sphere geometry to render equirectangular textures
const overlayGeometry = new Sphere(gl, { radius: 1, widthSegments: 64 });
const overlayProgram = new Program(gl, {
vertex: VertexShader,
fragment: FragmentShader,
uniforms: {
tMap: { value: overlayTexture },
opacity: { value: 0.5 }
},
transparent: true,
// Need inside of sphere to be visible
cullFace: null,
});
// Camera will dwell inside skybox
const overlay = new Mesh(gl, { geometry: overlayGeometry, program: overlayProgram });
overlay.scale.set(9.995);
overlay.scale.x = -9.995;
overlay.setParent(scene);
let activeTool = "pan";
let shouldBeRendering = false;
function update() {
if (!shouldBeRendering) return;
requestAnimationFrame(update);
controls.update();
renderer.render({ scene, camera });
}
function start() {
!hasConfirmed && Toast.confirm("Drawing does not work correctly in 3D at the moment").then(c => {
hasConfirmed = c;
})
if (shouldBeRendering) return;
updateOverlay();
shouldBeRendering = true;
update();
}
function updateOverlay() {
overlayProgram.uniforms.tMap.value.image = canvas2D.getImageData(0, 0, image.width, image.height).data;
}
function stop() {
shouldBeRendering = false;
}
function setTool(t) {
activeTool = t;
controls.enabled = t === "pan";
}
function setOpacity(o) {
overlayProgram.uniforms.opacity.value = o / 100;
}
return {
start, stop, setTool, updateOverlay, setOpacity
}
}

View File

@ -0,0 +1,358 @@
<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 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";
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>{topLeftY}|{my}</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>

View File

@ -0,0 +1,26 @@
<script>
export let activeTool = "pan";
const tools = ["pan", "brush", "erasor", "polygon"];
</script>
{#each tools as t}
<button on:click={() => (activeTool = t)} class:active={activeTool === t}>
{t}
</button>
{/each}
<style>
button.active {
background-color: red;
}
button {
width: 80%;
margin-left: 10%;
margin-bottom: 5px;
text-overflow: clip;
text-align: center;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,78 @@
<script>
export let activeColor = "ff0000";
const minRadius = 1;
const maxRadius = 100;
export let brushRadius = 10;
export let layerOpacity = 50;
const colors = ["ff0000", "00ff00", "0000ff", "ffff00", "00ffff", "ff00ff"];
function handleMouseWheel(e) {
brushRadius = Math.min(
Math.max(brushRadius - e.deltaY / 10, minRadius),
maxRadius
);
}
</script>
<svelte:window on:mousewheel={handleMouseWheel} />
<div class="wrapper">
<div class="color-wrapper">
{#each colors as c}
<div
class="color"
on:click={() => (activeColor = c)}
class:active={activeColor === c}
style={`background-color: #${c};`}
/>
{/each}
</div>
<div class="settings-wrapper">
<label for="brush-radius">Brush Radius</label>
<input
id="brush-radius"
type="range"
min={minRadius}
max={maxRadius}
bind:value={brushRadius}
/>
<!-- <label for="brush-radius">LayerOpacity</label>
<input
id="brush-radius"
type="range"
min="0"
max="100"
steps="1"
bind:value={layerOpacity}
/> -->
</div>
</div>
<style>
.wrapper {
height: 100%;
display: grid;
grid-template-columns: 1fr auto;
}
.color-wrapper {
}
.color {
display: inline-block;
height: 20px;
width: 20px;
margin: 5px;
}
.color.active {
outline: solid medium black;
}
.settings-wrapper {
display: flex;
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { fly } from "svelte/transition";
export let msg;
export let time;
//export let time;
export let type;
export let res;
export let rej;
@ -23,6 +23,8 @@
outline: solid thin black;
padding-top: 2.5px;
margin: 5px;
background-color: white;
z-index: 999;
}
.wrapper.type-warn {

View File

@ -15,5 +15,6 @@
position: fixed;
left: 0px;
bottom: 0px;
z-index: 999;
}
</style>

View File

@ -0,0 +1,8 @@
const urlCreator = window.URL || window.webkitURL;
export default (buff: ArrayBuffer, mimeType: string) => {
const blob = new Blob([buff], { type: mimeType });
const imageUrl = urlCreator.createObjectURL(blob);
return imageUrl;
}

View File

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

View File

@ -0,0 +1,25 @@
import bufToImageUrl from "./BuffToImg";
export default (f: File): Promise<Image> => new Promise(async (res, rej) => {
const arr = await f.arrayBuffer()
const img = document.createElement("img");
img.src = bufToImageUrl(arr, f.type);
img.onload = async () => {
res({
id: 0,
width: img.width,
height: img.height,
name: f.name,
filename: f.name,
type: f.type,
colors: [],
overlayData: new ArrayBuffer(0),
lastModified: f.lastModified,
data: arr
});
};
});

View File

@ -0,0 +1,3 @@
export { default as bufToImageUrl } from "./BuffToImg";
export { default as fileToImage } from "./FileToImage";
export { default as countPixels } from "./CountPixels";

View File

@ -3,6 +3,7 @@
height="82"
viewBox="0 0 82 82"
fill="none"
style="height: 100%;"
xmlns="http://www.w3.org/2000/svg"
>
<path

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 238 B

1
view/src/icons/index.ts Normal file
View File

@ -0,0 +1 @@
export { default as Cross } from "./Cross.svelte"

489
view/src/ogl.d.ts vendored Normal file
View File

@ -0,0 +1,489 @@
declare module 'ogl' {
export class Euler extends Array {
constructor(x?: number, y?: number, z?: number, order?: string);
x: number;
y: number;
z: number;
set: (x: number, y?: number, z?: number) => this;
copy: (v: Vec3) => this;
reorder: (order: string) => this;
fromRotationMatrix: (m: Mat3, order?: string) => this;
fromQuaternion: (q: Quat, order?: string) => this;
}
export class Color extends Array {
constructor(r?: number | string, g?: number, b?: number);
static hexToRGB: (hex: string) => number[];
}
export class Vec2 extends Array {
constructor(x?: number, y?: number);
x: number;
y: number;
set: (x: number, y?: number) => Vec2;
copy: () => Vec2;
add: (va: Vec2, vb: Vec2) => Vec2;
sub: (va: Vec2, vb: Vec2) => Vec2;
multiply: (v: number) => Vec2;
divide: (v: number) => Vec2;
len: () => number;
distance: (v: Vec2) => number;
squaredLen: () => number;
squaredDistance: (v: Vec2) => number;
negate: (v?: Vec2) => Vec2;
cross: (va: Vec2, vb: Vec2) => Vec2;
scale: (v: Vec2) => Vec2;
normalize: () => Vec2;
dot: (v: Vec2) => number;
equals: (v: Vec2) => boolean;
applyMatrix3: (mat3: Mat3) => Vec2;
applyMatrix4: (mat4: Mat4) => Vec2;
applyQuaternion: (q: Quat) => Vec2;
angle: (v: Vec2) => number;
lerp: (v: Vec2, t: number) => Vec2;
clone: () => Vec2;
fromArray: (array: number[], offset?: number) => Vec2;
toArray: (array?: Array<number>, offset?: number) => Vec2;
transformDirection: (mat4: Mat4) => Vec2;
}
export class Vec3 extends Array {
constructor(x?: number, y?: number, z?: number);
x: number;
y: number;
z: number;
set: (
x: number | [number, number, number] | Vec3,
y?: number,
z?: number,
) => this;
copy: () => Vec3;
add: (va: Vec3, vb?: Vec3) => Vec3;
sub: (va: Vec3, vb?: Vec3) => Vec3;
multiply: (v: number) => Vec3;
divide: (v: number) => Vec3;
len: () => number;
distance: (v: Vec3) => number;
squaredLen: () => number;
squaredDistance: (v: Vec3) => number;
negate: (v?: Vec3) => Vec3;
cross: (va: Vec3, vb: Vec3) => Vec3;
scale: (v: Vec3) => Vec3;
normalize: () => Vec3;
dot: (v: Vec3) => number;
equals: (v: Vec3) => boolean;
applyMatrix4: (mat4: Mat4) => Vec3;
applyQuaternion: (q: Quat) => Vec3;
angle: (v: Vec3) => number;
lerp: (v: Vec3, t: number) => Vec3;
clone: () => Vec3;
fromArray: (array: number[], offset?: number) => Vec3;
toArray: (array?: Array<number>, offset?: number) => number[];
transformDirection: (mat4: Mat4) => Vec3;
}
export class Mat3 extends Array {
constructor(
m00: number,
m01: number,
m02: number,
m10: number,
m11: number,
m12: number,
m20: number,
m21: number,
m22: number,
);
set: (
m00: number,
m01: number,
m02: number,
m10: number,
m11: number,
m12: number,
m20: number,
m21: number,
m22: number,
) => Mat3;
translate: (v: Vec3, m?: Mat3) => Mat3;
rotate: (v: Vec3, m?: Mat3) => Mat3;
scale: (v: Vec3, m?: Mat3) => Mat3;
multiply: (ma: Mat3, mb: Mat3) => Mat3;
identity: () => Mat3;
copy: (m: Mat3) => Mat3;
fromMatrix4: (m: Mat4) => Mat3;
fromQuaternion: (q: Quat) => Mat3;
fromBasis: (vec3a: Vec3, vec3b: Vec3, vec3c: Vec3) => Mat3;
inverse: (m?: Mat3) => Mat3;
getNormalMatrix: (m: Mat3) => Mat3;
}
export class Mat4 extends Array {
constructor(
m00: number,
m01: number,
m02: number,
m03: number,
m10: number,
m11: number,
m12: number,
m13: number,
m20: number,
m21: number,
m22: number,
m23: number,
m30: number,
m31: number,
m32: number,
m33: number,
);
x: number;
y: number;
z: number;
w: number;
set: (
m00: number,
m01: number,
m02: number,
m03: number,
m10: number,
m11: number,
m12: number,
m13: number,
m20: number,
m21: number,
m22: number,
m23: number,
m30: number,
m31: number,
m32: number,
m33: number,
) => Mat4;
}
export class Quat extends Array {
constructor(x?: number, y?: number, z?: number, w?: number);
x: number;
y: number;
z: number;
w: number;
identity: () => this;
set: (x: number, y: number, z: number, w: number) => this;
rotateX: (a: number) => this;
rotateY: (a: number) => this;
rotateZ: (a: number) => this;
inverse: (q?: Quat) => this;
conjugate: (q?: Quat) => this;
copy: (q: Quat) => this;
normalize: (q?: Quat) => this;
multiply: (qA: Quat, qB: Quat) => this;
dot: (v: Vec3) => number;
fromMatrix3: (matrix3: Mat3) => this;
fromEuler: (euler: number) => this;
fromAxisAngle: (axis: Vec3, a: number) => this;
fromArray: (a: number[], offset?: number) => this;
toArray: (a?: [], offset?: number) => number[];
}
export class Camera extends Transform {
constructor(
gl: WebGL2RenderingContext,
object: { near?: number; far?: number; fov?: number; aspect?: number },
);
perspective: (object: {
near?: number;
far?: number;
fov?: number;
aspect?: number;
}) => Camera;
orthographic: (object: {
near?: number;
far?: number;
fov?: number;
aspect?: number;
}) => Camera;
updateMatrixWorld: () => Camera;
lookAt: (target: Vec3) => Camera;
project: (v: Vec3) => Camera;
unproject: (v: Vec3) => Camera;
updateFrustum: () => void;
frustumIntersectsMesh: () => void;
frustumIntersectsSphere: () => void;
}
export class Transform {
position: Vec3;
quaternion: Quat;
scale: Vec3;
rotation: Euler;
up: Vec3;
setParent: (parent: Transform, notifyParent?: boolean) => void;
addChild: (parent: Transform, notifyChild?: boolean) => void;
removeChild: (parent: Transform, notifyChild?: boolean) => void;
updateMatrixWorld: (force?: boolean) => void;
updateMatrix: () => void;
traverse: (callback: () => Transform) => void;
decompose: () => void;
lookAt: (target: Vec3) => void;
}
export type AttributeMap = {
[key: string]: Partial<Attribute>;
};
export type Attribute = {
size: number;
data: ArrayLike<number> | ArrayBufferView;
instanced?: null | number;
type: GLenum;
normalized: boolean;
target?: number;
id?: number;
buffer?: WebGLBuffer;
stride: number;
offset: number;
count?: number;
divisor?: number;
needsUpdate?: boolean;
min?: unknown;
max?: unknown;
};
export class Program {
constructor(
gl: WebGL2RenderingContext,
config?: {
vertex: string;
fragment: string;
uniforms?: Record<string, unknown>;
depthTest?: boolean;
cullFace?: number;
transparent?: boolean;
},
);
uniforms: Record<string, unknown>;
setBlendFunc: () => void;
setBlendEquation: () => void;
applyState: () => void;
}
export class Renderer {
constructor(object?: {
canvas?: HTMLCanvasElement;
width?: number;
height?: number;
dpr?: number;
alpha?: boolean;
depth?: boolean;
stencil?: boolean;
antialias?: boolean;
premultipliedAlpha?: boolean;
preserveDrawingBuffer?: boolean;
powerPreference?: string;
autoClear?: boolean;
webgl?: number;
});
gl: WebGL2RenderingContext;
setSize: (width: number, height: number) => void;
setViewport: (width: number, height: number) => void;
render: (object: {
scene?: Transform;
camera?: Camera;
update?: boolean;
sort?: boolean;
frustumCull?: boolean;
}) => void;
}
export class Texture {
constructor(
gl: WebGL2RenderingContext,
object?: {
image?: HTMLImageElement | Uint8Array | Uint8ClampedArray;
generateMipmaps?: boolean;
premultiplyAlpha?: boolean;
unpackAlignment?: number;
wrapS?: number;
wrapT?: number;
flip?: boolean;
level?: number;
width?: number; // used for RenderTargets or Data Textures
height?: number;
},
);
image: HTMLImageElement;
}
export class Geometry {
constructor(gl: WebGL2RenderingContext, attributes: AttributeMap);
gl: WebGL2RenderingContext;
bounds: {
min: Vec3;
max: Vec3;
center: Vec3;
scale: Vec3;
radius: number;
};
attributes: AttributeMap;
setInstancedCount: (value: number) => void;
setIndex: (value: Uint16Array | Uint32Array) => void;
addAttribute: (key: string, attribute: AttributeMap) => void;
updateAttribute: (attr: AttributeMap) => void;
computeBoundingBox: (array?: number[]) => void;
setDrawRange: (start: number, count: number) => void;
remove: () => void;
}
export interface MeshOptions {
mode?: number;
geometry: Geometry;
program: Program;
}
export class Mesh extends Transform {
constructor(gl: WebGL2RenderingContext, object: Partial<MeshOptions>);
visible: boolean;
parent: Mesh;
program: Program;
mode: number;
geometry: Geometry;
draw: (camera: Camera) => void;
}
export interface PlaneOptions {
width: number;
height: number;
widthSegments: number;
heightSegments: number;
attributes: AttributeMap;
}
export class Plane extends Geometry {
constructor(gl: WebGL2RenderingContext, options?: Partial<PlaneOptions>);
}
export interface SphereOptions {
radius: number;
widthSegments: number;
heightSegments: number;
phiStart: number;
phiLength: number;
thetaStart: number;
thetaLength: number;
attributes: AttributeMap;
}
export class Sphere extends Geometry {
constructor(gl: WebGL2RenderingContext, object?: Partial<SphereOptions>);
}
export interface BoxOptions {
width: number;
height: number;
depth: number;
widthSegments: number;
heightSegments: number;
depthSegments: number;
attributes: AttributeMap;
}
export class Box extends Geometry {
constructor(gl: WebGL2RenderingContext, object?: Partial<BoxOptions>);
}
export class Orbit {
enabled: boolean;
constructor(
object: Camera,
config: {
element?: HTMLElement;
enabled?: boolean;
target?: Vec3;
ease?: number;
inertia?: number;
enableRotate?: boolean;
rotateSpeed?: number;
enableZoom?: boolean;
zoomSpeed?: number;
enablePan?: boolean;
panSpeed?: number;
minPolarAngle?: number;
maxPolarAngle?: number;
minAzimuthAngle?: number;
maxAzimuthAngle?: number;
minDistance?: number;
maxDistance?: number;
},
);
target: Vec3;
update: () => void;
}
}

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { bufToImageUrl } from "helpers";
import { route as currentRoute, images as imageStore } from "stores";
import Editor from "components/Editor/Editor.svelte";
const imageId = parseInt($currentRoute.split("/")[1]);
const imagePromise = imageStore.get(imageId);
</script>
{#await imagePromise}
Loading image
{:then image}
<Editor {image} />
{:catch err}
<p>Error loading image</p>
{JSON.stringify(err)}
{/await}

View File

@ -0,0 +1,2 @@
<script>
</script>

View File

@ -1,24 +1,133 @@
<script lang="ts">
import { store as imageStore } from "../stores/images";
import { Cross } from "../icons";
import { fly, fade } from "svelte/transition";
import { images as imageData, route } from "stores";
import { bufToImageUrl } from "helpers";
import type { Writable } from "svelte/store";
let images = [];
const urlCreator = window.URL || window.webkitURL;
import Toast from "../components/Toast";
import Analyzer from "../components/Analyzer.svelte";
import { onMount } from "svelte";
$: if ($imageStore.length) {
images = $imageStore.map((img) => {
const blob = new Blob([img.data], { type: img.type });
const imageUrl = urlCreator.createObjectURL(blob);
return {
...img,
imageUrl,
};
});
const imageStore: Writable<Image[]> = imageData.store;
let showAnalyzerIndex;
function handleFileChange(ev, img: Image) {
if (ev.target.innerText.includes("\n")) {
const value = ev.target.innerText.replace("\n", "");
ev.target.innerText = value;
ev.target.blur();
img.name = value;
imageData.updateImage(img);
}
}
let startCheckingEmpty = false;
setTimeout(() => {
startCheckingEmpty = true;
}, 1000);
onMount(() =>
imageStore.subscribe((img) => {
if (img.length === 0 && startCheckingEmpty) {
route.set("main");
}
})
);
</script>
{#each images as img}
<p>{img.filename}</p>
<img src={img.imageUrl} />
{/each}
<main>
<h3>List</h3>
<h3>List</h3>
{#each $imageStore as img, i}
<div class="list-item" in:fly={{ duration: 500, x: -50, delay: i * 200 }}>
<img
alt={img.filename}
on:click={() => route.set("editor/" + img.id)}
src={bufToImageUrl(img.data, img.type)}
/>
<div class="content">
<h4 contenteditable on:input={(ev) => handleFileChange(ev, img)}>
{img.name}
</h4>
<button
on:click={() => {
if (img.overlayData && img.overlayData.byteLength) {
if (showAnalyzerIndex === i) {
showAnalyzerIndex = undefined;
} else {
showAnalyzerIndex = i;
}
} else {
showAnalyzerIndex = undefined;
Toast.warn("Image has no data; Paint some regions to analyze");
}
}}>analyze</button
>
<button on:click={() => route.set("editor/" + img.id)}>edit</button>
<button
on:click={() => {
imageData.deleteImage(img);
}}>delete</button
>
</div>
</div>
{#if i === showAnalyzerIndex}
<div transition:fly={{ x: -50 }}>
<Analyzer {img} />
</div>
{/if}
{/each}
<div id="cross-wrapper" on:click={() => route.set("main")}>
<Cross />
<p>Add more images</p>
</div>
</main>
<style>
#cross-wrapper {
margin-top: 40px;
display: flex;
align-items: center;
}
#cross-wrapper > p {
margin-left: 10px;
}
main {
height: 100%;
max-width: 640px;
margin: 0 auto;
}
@media (max-width: 640px) {
main {
max-width: none;
}
}
.list-item {
position: relative;
display: grid;
grid-template-columns: 80px auto;
column-gap: 10px;
margin-top: 10px;
outline: solid thin black;
}
.list-item > img {
width: 80px;
height: 100%;
object-fit: cover;
cursor: pointer;
background-color: black;
transition: opacity 0.3s ease;
}
.list-item > img:hover {
opacity: 0.7;
}
</style>

View File

@ -3,10 +3,13 @@
import AnimatedNumber from "../components/AnimatedNumber.svelte";
import Cross from "../icons/Cross.svelte";
import Toast from "../components/Toast";
import { route as currentRoute, images } from "../stores";
import { route as currentRoute, images, route } from "../stores";
import { fileToImage } from "../helpers";
let acceptedFiles: File[] = [];
let hovering = false;
const imageStore = images.store;
async function handleFilesSelect(e) {
if (e.detail.acceptedFiles) {
const addedFiles: File[] = e.detail.acceptedFiles;
@ -14,13 +17,15 @@
const newFiles: File[] = [];
const dupes: File[] = [];
const allImages = [...$imageStore, ...acceptedFiles];
addedFiles.forEach((f) => {
const isNew = !acceptedFiles.find((_f) => {
const isNew = !allImages.find((_f: Image) => {
return (
_f.lastModified === f.lastModified &&
_f.name === f.name &&
_f.type === f.type &&
_f.size === f.size
_f.data.byteLength === f.size
);
});
@ -35,9 +40,9 @@
if (dupes.length) {
const loadDupes = await Toast.confirm(
`Add <b> ${dupes.length}</b> duplicate file${
dupes.length > 1 ? "s" : ""
}?`
`Some images are already loaded. Add <b> ${
dupes.length
}</b> duplicate${dupes.length > 1 ? "s" : ""}?`
);
if (loadDupes) {
@ -53,17 +58,7 @@
e.stopImmediatePropagation();
e.stopPropagation();
const img = await Promise.all(
acceptedFiles.map(async (f) => {
return {
name: f.name,
filename: f.name,
type: f.type,
lastModified: f.lastModified,
data: await f.arrayBuffer(),
};
})
);
const img = await Promise.all(acceptedFiles.map(fileToImage));
images.add(img);
@ -78,7 +73,7 @@
}
</script>
<div class="wrapper">
<div id="wrapper">
<DropZone
accept="image/*"
on:drop={handleFilesSelect}
@ -86,6 +81,12 @@
on:dragleave={() => (hovering = false)}
on:dragenter={() => (hovering = false)}
>
{#if $imageStore.length}
<button on:click|preventDefault|stopPropagation={() => route.set("list")}
>Exit</button
>
{/if}
<div class="inner-wrapper">
<div class="icon-wrapper" class:hovering>
<Cross />
@ -103,7 +104,8 @@
</div>
<style>
.wrapper {
#wrapper {
position: relative;
display: grid;
place-items: center;
height: 100%;
@ -114,6 +116,7 @@
display: flex;
align-items: center;
padding: 20px;
outline: solid thin black;
}
p {

View File

@ -1,7 +0,0 @@
import { writable } from "svelte/store";
const r = "route" in localStorage ? localStorage.getItem("route"):"main";
const store = writable<string>(r);
export default store;

View File

@ -1,18 +1,12 @@
import { writable } from "svelte/store";
import Toast from "../components/Toast"
import { openDB, DBSchema } from 'idb';
interface Image {
name: string,
filename: string
lastModified: number
type: string
data: ArrayBuffer
}
interface ImageDB extends DBSchema {
images: {
value: Image;
key: string;
key: number;
indexes: { 'filename': string };
};
}
@ -38,26 +32,46 @@ const store = writable<Image[]>([]);
async function add(img: Image | Image[]) {
const images = Array.isArray(img) ? [...img] : [img];
if (!images.length) return;
const tx = (await db).transaction('images', 'readwrite');
await Promise.all(images.map(img => tx.store.add(img)));
await Promise.all(images.map(img => {
delete img.id;
return tx.store.add(img);
}));
Toast.info("Loaded <b>" + images.length + "</b> images")
store.set(await getAll())
await tx.done;
}
async function get(name: string): Promise<Image> {
return new Promise((res, rej) => {
res({} as Image);
})
async function updateImage(img: Image) {
img.lastModified = Date.now();
(await db).put("images", img);
store.update(images => images.map(i => i.id === img.id ? img : i));
}
async function get(id: number): Promise<Image> {
return (await db).get("images", id);
}
async function getAll(): Promise<Image[]> {
return (await db).getAll("images");
}
(async () => store.set(await getAll()))()
async function deleteImage(img: Image): Promise<void> {
(await db).delete("images", img.id);
store.update(images => images.filter(i => i.id !== img.id));
}
export { store, add, get, getAll };
(async () => {
store.set(await getAll())
})()
export { store, add, get, getAll, updateImage, deleteImage };

View File

@ -1,18 +1,22 @@
import { writable } from "svelte/store";
let r = "main";
if (window.location.hash.length) {
r = window.location.hash.replace("#", "");
} else {
r = "main";
const getHash = () => {
if (window.location.hash.length) {
return window.location.hash.replace("#", "");
} else {
return "main";
}
}
const store = writable<string>(r);
const store = writable<string>(getHash());
store.subscribe(s => {
if (s === "main") s = ""
window.location.hash = s;
})
window.addEventListener("hashchange", () => {
store.set(getHash())
}, false);
export default store;

22
view/src/types.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
interface Image {
id: number;
width: number;
height: number;
name: string,
filename: string
lastModified: number
type: string
colors: string[]
overlayData: ArrayBuffer
data: ArrayBuffer
}
declare module "*.frag" {
const content: string;
export default content;
}
declare module "*.vert" {
const content: string;
export default content;
}

View File

@ -1,6 +1,9 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compileOptions":{
"baseUrl": "src",
},
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}