second day
This commit is contained in:
parent
7fc8feb0cc
commit
521e2a4eb1
4
Makefile
4
Makefile
@ -9,3 +9,7 @@ dev-server:
|
||||
dev-view:
|
||||
cd view && npm run dev
|
||||
|
||||
build: build-view
|
||||
|
||||
build-view:
|
||||
cd view && npm run build
|
1238
view/package-lock.json
generated
1238
view/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
|
@ -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
29
view/public/worker.js
Normal 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);
|
@ -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 -
|
||||
|
@ -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}
|
||||
{#if $currentRoute.startsWith("editor")}
|
||||
<Editor />
|
||||
{:else if $currentRoute in routes}
|
||||
<svelte:component this={routes[$currentRoute]} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<ToastWrapper />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
main {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<ToastWrapper />
|
||||
|
78
view/src/components/Analyzer.svelte
Normal file
78
view/src/components/Analyzer.svelte
Normal 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>
|
@ -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>
|
||||
|
2
view/src/components/Editor/Compass.svelte
Normal file
2
view/src/components/Editor/Compass.svelte
Normal file
@ -0,0 +1,2 @@
|
||||
<script>
|
||||
</script>
|
331
view/src/components/Editor/CustomOrbit.ts
Normal file
331
view/src/components/Editor/CustomOrbit.ts
Normal 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();
|
||||
}
|
77
view/src/components/Editor/Editor.svelte
Normal file
77
view/src/components/Editor/Editor.svelte
Normal 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")}><</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>
|
8
view/src/components/Editor/Orb.frag
Normal file
8
view/src/components/Editor/Orb.frag
Normal 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;
|
||||
}
|
10
view/src/components/Editor/Orb.vert
Normal file
10
view/src/components/Editor/Orb.vert
Normal 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);
|
||||
}
|
136
view/src/components/Editor/OrbView.ts
Normal file
136
view/src/components/Editor/OrbView.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
358
view/src/components/Editor/Painter.svelte
Normal file
358
view/src/components/Editor/Painter.svelte
Normal 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>
|
26
view/src/components/Editor/ToolBox.svelte
Normal file
26
view/src/components/Editor/ToolBox.svelte
Normal 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>
|
78
view/src/components/Editor/TopBar.svelte
Normal file
78
view/src/components/Editor/TopBar.svelte
Normal 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>
|
@ -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 {
|
||||
|
@ -15,5 +15,6 @@
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
|
8
view/src/helpers/BuffToImg.ts
Normal file
8
view/src/helpers/BuffToImg.ts
Normal 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;
|
||||
}
|
18
view/src/helpers/CountPixels.ts
Normal file
18
view/src/helpers/CountPixels.ts
Normal 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;
|
||||
});
|
25
view/src/helpers/FileToImage.ts
Normal file
25
view/src/helpers/FileToImage.ts
Normal 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
|
||||
});
|
||||
};
|
||||
|
||||
});
|
3
view/src/helpers/index.ts
Normal file
3
view/src/helpers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as bufToImageUrl } from "./BuffToImg";
|
||||
export { default as fileToImage } from "./FileToImage";
|
||||
export { default as countPixels } from "./CountPixels";
|
@ -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
1
view/src/icons/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Cross } from "./Cross.svelte"
|
489
view/src/ogl.d.ts
vendored
Normal file
489
view/src/ogl.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
18
view/src/routes/editor.svelte
Normal file
18
view/src/routes/editor.svelte
Normal 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}
|
2
view/src/routes/error.svelte
Normal file
2
view/src/routes/error.svelte
Normal file
@ -0,0 +1,2 @@
|
||||
<script>
|
||||
</script>
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
@ -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 };
|
@ -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
22
view/src/types.d.ts
vendored
Normal 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;
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"compileOptions":{
|
||||
"baseUrl": "src",
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user