second day
This commit is contained in:
parent
7fc8feb0cc
commit
521e2a4eb1
6
Makefile
6
Makefile
@ -8,4 +8,8 @@ dev-server:
|
|||||||
cd server && gin run main.go
|
cd server && gin run main.go
|
||||||
dev-view:
|
dev-view:
|
||||||
cd view && npm run dev
|
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"
|
"validate": "svelte-check"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-alias": "^3.1.2",
|
||||||
"@rollup/plugin-commonjs": "^17.0.0",
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||||
"@rollup/plugin-typescript": "^8.0.0",
|
"@rollup/plugin-typescript": "^8.0.0",
|
||||||
@ -16,6 +17,8 @@
|
|||||||
"idb": "^6.0.0",
|
"idb": "^6.0.0",
|
||||||
"rollup": "^2.3.4",
|
"rollup": "^2.3.4",
|
||||||
"rollup-plugin-css-only": "^3.1.0",
|
"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-livereload": "^2.0.0",
|
||||||
"rollup-plugin-svelte": "^7.0.0",
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.0",
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
@ -26,6 +29,7 @@
|
|||||||
"typescript": "^4.0.0"
|
"typescript": "^4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ogl": "^0.0.65",
|
||||||
"sirv-cli": "^1.0.0",
|
"sirv-cli": "^1.0.0",
|
||||||
"svelte-file-dropzone": "^0.0.15"
|
"svelte-file-dropzone": "^0.0.15"
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@
|
|||||||
|
|
||||||
<title>Karls Analyzer</title>
|
<title>Karls Analyzer</title>
|
||||||
|
|
||||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
<link rel='icon' type='image/png' href='favicon.png'>
|
||||||
<link rel='stylesheet' href='/global.css'>
|
<link rel='stylesheet' href='global.css'>
|
||||||
<link rel='stylesheet' href='/build/bundle.css'>
|
<link rel='stylesheet' href='build/bundle.css'>
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;900&display=swap" rel="stylesheet">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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 sveltePreprocess from 'svelte-preprocess';
|
||||||
import typescript from '@rollup/plugin-typescript';
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import css from 'rollup-plugin-css-only';
|
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;
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -17,6 +18,12 @@ export default {
|
|||||||
file: 'public/build/bundle.js'
|
file: 'public/build/bundle.js'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
||||||
|
includePaths({
|
||||||
|
paths: ["src"],
|
||||||
|
extensions: [".ts", ".svelte"]
|
||||||
|
}),
|
||||||
|
|
||||||
svelte({
|
svelte({
|
||||||
preprocess: sveltePreprocess({ sourceMap: !production }),
|
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
@ -28,6 +35,8 @@ export default {
|
|||||||
// a separate file - better for performance
|
// a separate file - better for performance
|
||||||
css({ output: 'bundle.css' }),
|
css({ output: 'bundle.css' }),
|
||||||
|
|
||||||
|
glslify(),
|
||||||
|
|
||||||
// If you have external dependencies installed from
|
// If you have external dependencies installed from
|
||||||
// npm, you'll most likely need these plugins. In
|
// npm, you'll most likely need these plugins. In
|
||||||
// some cases you'll need additional configuration -
|
// some cases you'll need additional configuration -
|
||||||
|
@ -1,25 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Editor from "./routes/editor.svelte";
|
||||||
import * as routes from "./routes";
|
import * as routes from "./routes";
|
||||||
import ToastWrapper from "./components/Toast/ToastWrapper.svelte";
|
import ToastWrapper from "./components/Toast/ToastWrapper.svelte";
|
||||||
import { route as currentRoute } from "./stores";
|
import { route as currentRoute } from "./stores";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
{#if $currentRoute.startsWith("editor")}
|
||||||
{#if $currentRoute in routes}
|
<Editor />
|
||||||
<svelte:component this={routes[$currentRoute]} />
|
{:else if $currentRoute in routes}
|
||||||
{/if}
|
<svelte:component this={routes[$currentRoute]} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ToastWrapper />
|
<ToastWrapper />
|
||||||
</main>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
main {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
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 containerClasses = "";
|
||||||
export let containerStyles = "";
|
export let containerStyles = "";
|
||||||
export let disableDefaultStyles = false;
|
export let disableDefaultStyles = false;
|
||||||
|
export let disableClick = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@ -311,7 +312,7 @@
|
|||||||
on:keydown={composeKeyboardHandler(onKeyDownCb)}
|
on:keydown={composeKeyboardHandler(onKeyDownCb)}
|
||||||
on:focus={composeKeyboardHandler(onFocusCb)}
|
on:focus={composeKeyboardHandler(onFocusCb)}
|
||||||
on:blur={composeKeyboardHandler(onBlurCb)}
|
on:blur={composeKeyboardHandler(onBlurCb)}
|
||||||
on:click={composeHandler(onClickCb)}
|
on:click={!disableClick && composeHandler(onClickCb)}
|
||||||
on:dragenter={composeDragHandler(onDragEnterCb)}
|
on:dragenter={composeDragHandler(onDragEnterCb)}
|
||||||
on:dragover={composeDragHandler(onDragOverCb)}
|
on:dragover={composeDragHandler(onDragOverCb)}
|
||||||
on:dragleave={composeDragHandler(onDragLeaveCb)}
|
on:dragleave={composeDragHandler(onDragLeaveCb)}
|
||||||
@ -324,27 +325,9 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
on:change={onDropCb}
|
on:change={onDropCb}
|
||||||
on:click={onInputElementClick}
|
on:click={!disableClick && onInputElementClick}
|
||||||
bind:this={inputRef}
|
bind:this={inputRef}
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
/>
|
/>
|
||||||
<slot>
|
<slot />
|
||||||
<p>Drag 'n' drop some files here, or click to select files</p>
|
|
||||||
</slot>
|
|
||||||
</div>
|
</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">
|
<script lang="ts">
|
||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
export let msg;
|
export let msg;
|
||||||
export let time;
|
//export let time;
|
||||||
export let type;
|
export let type;
|
||||||
export let res;
|
export let res;
|
||||||
export let rej;
|
export let rej;
|
||||||
@ -23,6 +23,8 @@
|
|||||||
outline: solid thin black;
|
outline: solid thin black;
|
||||||
padding-top: 2.5px;
|
padding-top: 2.5px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper.type-warn {
|
.wrapper.type-warn {
|
||||||
|
@ -15,5 +15,6 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
</style>
|
</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"
|
height="82"
|
||||||
viewBox="0 0 82 82"
|
viewBox="0 0 82 82"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
style="height: 100%;"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<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">
|
<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 = [];
|
import Toast from "../components/Toast";
|
||||||
const urlCreator = window.URL || window.webkitURL;
|
import Analyzer from "../components/Analyzer.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
$: if ($imageStore.length) {
|
const imageStore: Writable<Image[]> = imageData.store;
|
||||||
images = $imageStore.map((img) => {
|
|
||||||
const blob = new Blob([img.data], { type: img.type });
|
let showAnalyzerIndex;
|
||||||
const imageUrl = urlCreator.createObjectURL(blob);
|
|
||||||
return {
|
function handleFileChange(ev, img: Image) {
|
||||||
...img,
|
if (ev.target.innerText.includes("\n")) {
|
||||||
imageUrl,
|
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>
|
</script>
|
||||||
|
|
||||||
{#each images as img}
|
<main>
|
||||||
<p>{img.filename}</p>
|
<h3>List</h3>
|
||||||
<img src={img.imageUrl} />
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<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 AnimatedNumber from "../components/AnimatedNumber.svelte";
|
||||||
import Cross from "../icons/Cross.svelte";
|
import Cross from "../icons/Cross.svelte";
|
||||||
import Toast from "../components/Toast";
|
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 acceptedFiles: File[] = [];
|
||||||
let hovering = false;
|
let hovering = false;
|
||||||
|
|
||||||
|
const imageStore = images.store;
|
||||||
|
|
||||||
async function handleFilesSelect(e) {
|
async function handleFilesSelect(e) {
|
||||||
if (e.detail.acceptedFiles) {
|
if (e.detail.acceptedFiles) {
|
||||||
const addedFiles: File[] = e.detail.acceptedFiles;
|
const addedFiles: File[] = e.detail.acceptedFiles;
|
||||||
@ -14,13 +17,15 @@
|
|||||||
const newFiles: File[] = [];
|
const newFiles: File[] = [];
|
||||||
const dupes: File[] = [];
|
const dupes: File[] = [];
|
||||||
|
|
||||||
|
const allImages = [...$imageStore, ...acceptedFiles];
|
||||||
|
|
||||||
addedFiles.forEach((f) => {
|
addedFiles.forEach((f) => {
|
||||||
const isNew = !acceptedFiles.find((_f) => {
|
const isNew = !allImages.find((_f: Image) => {
|
||||||
return (
|
return (
|
||||||
_f.lastModified === f.lastModified &&
|
_f.lastModified === f.lastModified &&
|
||||||
_f.name === f.name &&
|
_f.name === f.name &&
|
||||||
_f.type === f.type &&
|
_f.type === f.type &&
|
||||||
_f.size === f.size
|
_f.data.byteLength === f.size
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,9 +40,9 @@
|
|||||||
|
|
||||||
if (dupes.length) {
|
if (dupes.length) {
|
||||||
const loadDupes = await Toast.confirm(
|
const loadDupes = await Toast.confirm(
|
||||||
`Add <b> ${dupes.length}</b> duplicate file${
|
`Some images are already loaded. Add <b> ${
|
||||||
dupes.length > 1 ? "s" : ""
|
dupes.length
|
||||||
}?`
|
}</b> duplicate${dupes.length > 1 ? "s" : ""}?`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loadDupes) {
|
if (loadDupes) {
|
||||||
@ -53,17 +58,7 @@
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const img = await Promise.all(
|
const img = await Promise.all(acceptedFiles.map(fileToImage));
|
||||||
acceptedFiles.map(async (f) => {
|
|
||||||
return {
|
|
||||||
name: f.name,
|
|
||||||
filename: f.name,
|
|
||||||
type: f.type,
|
|
||||||
lastModified: f.lastModified,
|
|
||||||
data: await f.arrayBuffer(),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
images.add(img);
|
images.add(img);
|
||||||
|
|
||||||
@ -78,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div id="wrapper">
|
||||||
<DropZone
|
<DropZone
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
on:drop={handleFilesSelect}
|
on:drop={handleFilesSelect}
|
||||||
@ -86,6 +81,12 @@
|
|||||||
on:dragleave={() => (hovering = false)}
|
on:dragleave={() => (hovering = false)}
|
||||||
on:dragenter={() => (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="inner-wrapper">
|
||||||
<div class="icon-wrapper" class:hovering>
|
<div class="icon-wrapper" class:hovering>
|
||||||
<Cross />
|
<Cross />
|
||||||
@ -103,7 +104,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper {
|
#wrapper {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -114,6 +116,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
outline: solid thin black;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
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 { writable } from "svelte/store";
|
||||||
|
import Toast from "../components/Toast"
|
||||||
import { openDB, DBSchema } from 'idb';
|
import { openDB, DBSchema } from 'idb';
|
||||||
|
|
||||||
interface Image {
|
|
||||||
name: string,
|
|
||||||
filename: string
|
|
||||||
lastModified: number
|
|
||||||
type: string
|
|
||||||
data: ArrayBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImageDB extends DBSchema {
|
interface ImageDB extends DBSchema {
|
||||||
images: {
|
images: {
|
||||||
value: Image;
|
value: Image;
|
||||||
key: string;
|
key: number;
|
||||||
indexes: { 'filename': string };
|
indexes: { 'filename': string };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -38,26 +32,46 @@ const store = writable<Image[]>([]);
|
|||||||
|
|
||||||
async function add(img: Image | Image[]) {
|
async function add(img: Image | Image[]) {
|
||||||
|
|
||||||
|
|
||||||
const images = Array.isArray(img) ? [...img] : [img];
|
const images = Array.isArray(img) ? [...img] : [img];
|
||||||
|
if (!images.length) return;
|
||||||
|
|
||||||
const tx = (await db).transaction('images', 'readwrite');
|
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;
|
await tx.done;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(name: string): Promise<Image> {
|
async function updateImage(img: Image) {
|
||||||
return new Promise((res, rej) => {
|
img.lastModified = Date.now();
|
||||||
res({} as Image);
|
(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[]> {
|
async function getAll(): Promise<Image[]> {
|
||||||
return (await db).getAll("images");
|
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";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
let r = "main";
|
const getHash = () => {
|
||||||
|
if (window.location.hash.length) {
|
||||||
if (window.location.hash.length) {
|
return window.location.hash.replace("#", "");
|
||||||
r = window.location.hash.replace("#", "");
|
} else {
|
||||||
} else {
|
return "main";
|
||||||
r = "main";
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = writable<string>(r);
|
const store = writable<string>(getHash());
|
||||||
|
|
||||||
store.subscribe(s => {
|
store.subscribe(s => {
|
||||||
if (s === "main") s = ""
|
if (s === "main") s = ""
|
||||||
window.location.hash = s;
|
window.location.hash = s;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", () => {
|
||||||
|
store.set(getHash())
|
||||||
|
}, false);
|
||||||
|
|
||||||
export default store;
|
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",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
|
||||||
|
"compileOptions":{
|
||||||
|
"baseUrl": "src",
|
||||||
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user