Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 배치도 구역을 캔버스로 렌더링하도록 변경 #346

Merged
merged 14 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/aws-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
python-version: "3.10"
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
- uses: aws-actions/setup-sam@v2
- uses: aws-actions/configure-aws-credentials@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 21
- name: Enable corepack
run: corepack enable
- name: Install pnpm
Expand Down
9 changes: 6 additions & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/forms": "^0.5.7",
"@types/lodash.isequal": "^4.5.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.19",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-svelte": "^2.42.0",
"eslint-plugin-tailwindcss": "^3.17.4",
"konva": "^9.3.13",
"postcss": "^8.4.39",
"postcss-load-config": "^5.1.0",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"svelte": "^4.2.18",
"svelte-check": "^3.8.4",
"svelte-konva": "^0.3.1",
"svelte-preprocess": "^6.0.2",
"tailwindcss": "^3.4.5",
"tslib": "^2.6.3",
Expand All @@ -46,6 +48,7 @@
},
"type": "module",
"dependencies": {
"canvas": "^2.11.2",
"lodash.isequal": "^4.5.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.10/xlsx-0.18.10.tgz"
}
Expand Down
54 changes: 0 additions & 54 deletions packages/client/src/components/atom/FloorMap.svelte

This file was deleted.

259 changes: 259 additions & 0 deletions packages/client/src/components/atom/MapCanvas.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import {
Image,
type KonvaDragTransformEvent,
type KonvaTouchEvent,
type KonvaWheelEvent,
Layer,
Stage,
} from 'svelte-konva';
import type { Stage as StageHandle } from 'konva/lib/Stage';
import { beforeUpdate, onMount } from 'svelte';
import Skeleton from './Skeleton.svelte';
import Button from './Button.svelte';
import ArrowClockwise from '../../icons/ArrowClockwise.svelte';
import Konva from 'konva';
import type { IFrame } from 'konva/lib/types';
import { sineInOut } from 'svelte/easing';

let clazz = '';
export { clazz as class };
export let alt = '배치도';
export let src: string;
export let highlightSrc: string = null;
export let highlightX: number = 0;
export let highlightY: number = 0;

let parent: HTMLDivElement;
let stage: StageHandle;
let image: HTMLImageElement;
let highlightImage: HTMLImageElement;
let highlight: Konva.Image;
let width: number;
let height: number;
let resizeCallback: NodeJS.Timeout;
let imageRatio: number;
let zoomScale = 1;
let stageX = 0;
let stageY = 0;
let defaultScale: number;
let mounted: boolean = false;
let highlightAnimation: Konva.Animation = null;

$: canvasRatio = width && height && width / height;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function resizeCanvas(_entries: ResizeObserverEntry[], _observer: ResizeObserver) {
if (resizeCallback) clearTimeout(resizeCallback);
if (parent && (width !== parent.clientWidth || height !== parent.clientHeight)) {
width = undefined;
height = undefined;
resizeCallback = setTimeout(function() {
if (parent) {
width = parent?.clientWidth;
height = parent?.clientHeight;
}
resizeCallback = undefined;
}, 500);
}
}

function getDistance(p1: { x: number, y: number }, p2: { x: number, y: number }): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}

function getCenter(p1: { x: number, y: number }, p2: { x: number, y: number }): { x: number, y: number } {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
}

let dragStopped: boolean = false;
let lastCenter: { x: number, y: number } = null;
let lastDist: number = null;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function resetTouchPosition(_e: KonvaTouchEvent) {
lastCenter = null;
lastDist = null;
}

function zoomMapWithTouch(e: KonvaTouchEvent): void {
e.detail.evt.preventDefault();
const touch1 = e.detail.evt.touches[0];
const touch2 = e.detail.evt.touches[1];

if (touch1 && !touch2 && !stage.isDragging() && dragStopped) {
stage.startDrag();
dragStopped = false;

}

if (touch1 && touch2) {
if (!dragStopped) {
dragStopped = true;
stage.stopDrag();
}
const p1 = { x: touch1.clientX, y: touch1.clientY };
const p2 = { x: touch2.clientX, y: touch2.clientY };
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
const newCenter = getCenter(p1, p2);
const dist = getDistance(p1, p2);

if (!lastDist) {
lastDist = dist;
}

const oldScale = zoomScale;

const pointTo = {
x: (newCenter.x - stage.x()) / oldScale,
y: (newCenter.y - stage.y()) / oldScale,
};

console.log(oldScale, ((dist / lastDist)));
const newScale = oldScale * (dist / lastDist);

const dx = newCenter.x - lastCenter.x;
const dy = newCenter.y - lastCenter.y;

const newPos = {
x: newCenter.x - pointTo.x * newScale + dx,
y: newCenter.y - pointTo.y * newScale + dy,
};

if (newScale <= defaultScale * 4.0 && newScale >= defaultScale * 0.25) {
zoomScale = newScale;
}

stageX = newPos.x;
stageY = newPos.y;

lastDist = dist;
lastCenter = newCenter;
}
}


function zoomMapWithWheel(e: KonvaWheelEvent): void {
// Enable zoom only when user scroll with ctrl key
if (e.detail.evt.ctrlKey) {
e.detail.evt.preventDefault();
let oldScale = zoomScale;
const pointer = stage.getPointerPosition();

const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};

let direction = e.detail.evt.deltaY > 0 ? -1 : 1;

let newScale = direction > 0 ? oldScale * 1.1 : oldScale / 1.1;
// Allow zoom level from 0.25 to 4.0
if (newScale <= defaultScale * 4.0 && newScale >= defaultScale * 0.25) {
zoomScale = newScale;
stageX = pointer.x - mousePointTo.x * zoomScale;
stageY = pointer.y - mousePointTo.y * zoomScale;
}
}
}

function reset() {
stageX = (width && image.width && defaultScale) ? (width - (image.width * defaultScale)) / 2 : 0;
stageY = 0;
zoomScale = defaultScale;
}

function updateStageCoords(e: KonvaDragTransformEvent): void {
stageX = e.detail.target.x();
stageY = e.detail.target.y();
}

onMount(() => {
width = parent?.clientWidth;
height = parent?.clientHeight;
new ResizeObserver(resizeCanvas).observe(parent);
mounted = true;
});

beforeUpdate(() => {
if (highlightAnimation) highlightAnimation.stop();
highlightAnimation = null;
});

$: if (mounted && src) {
const img = document.createElement('img');
img.src = src;
img.onload = () => {
image = img;
imageRatio = img.width / img.height;
};
}

$: if (mounted && highlightSrc) {
const highlightImg = document.createElement('img');
highlightImg.src = highlightSrc;
highlightImg.onload = () => {
highlightImage = highlightImg;
};
// TODO: move camera to highlighted position
}

const easing = (currentTime: number, repeatTime: number) => {
const flowVal = Math.abs((currentTime % repeatTime) - (repeatTime / 2)) / (repeatTime / 2); // 0~1
return sineInOut(flowVal);
};

$: if (highlight) {
if (highlightAnimation) highlightAnimation.stop();
highlightAnimation = new Konva.Animation((frame: IFrame) => {
highlight.setAttr('opacity', (easing(frame.time, 3000) * 0.8) + 0.2);
});
highlightAnimation.start();
}

$: if (imageRatio && canvasRatio) {
defaultScale = canvasRatio >= imageRatio ? height / image.height : width / image.width;
zoomScale = defaultScale;
reset();
}
</script>

<div
bind:this={parent}
class="{clazz} relative h-full w-full cursor-grab"
in:fly={{ y: 100, duration: 300 }}
aria-label={alt}>
{#if !isNaN(width) && !isNaN(height)}
{#key `${width};${height}`}
<Stage
bind:handle={stage}
config={{ width, height, scale: { x: zoomScale, y: zoomScale }, x: stageX, y: stageY, draggable: true }}
on:wheel={zoomMapWithWheel}
on:dragend={updateStageCoords}
on:touchmove={zoomMapWithTouch}
on:touchend={resetTouchPosition}
>
<Layer>
<Image config={{ image, x: 0, y: 0 }}></Image>
{#if highlightImage}
<Image config={{ image: highlightImage, x: highlightX, y: highlightY }} bind:handle={highlight}></Image>
{/if}
</Layer>
</Stage>
{/key}
{#if stageX !== 0 || stageY !== 0 || zoomScale !== defaultScale}
<Button class="absolute bottom-0 right-0 m-4 bg-white" on:click={reset}>
<ArrowClockwise />
</Button>
{/if}
{:else}
<Skeleton class="w-full h-full rounded-xl bg-gray-300" />
{/if}
</div>
Loading
Loading