|
@@ -20,6 +20,9 @@
|
|
|
:track-list="item.trackList"
|
|
|
:special-tag-list="item.tagList"
|
|
|
:original-image="item.originalImage"
|
|
|
+ :slice-image="item.sliceImage"
|
|
|
+ :dx="item.dx"
|
|
|
+ :dy="item.dy"
|
|
|
/>
|
|
|
<hr class="image-seperator" />
|
|
|
</div>
|
|
@@ -35,7 +38,11 @@ import filters from "@/filters";
|
|
|
import MarkDrawTrack from "./MarkDrawTrack.vue";
|
|
|
import { SpecialTag, Track } from "@/types";
|
|
|
import { useTimers } from "@/setups/useTimers";
|
|
|
-import { loadImage } from "@/utils/utils";
|
|
|
+import {
|
|
|
+ getDataUrlForSliceConfig,
|
|
|
+ getDataUrlForSplitConfig,
|
|
|
+ loadImage,
|
|
|
+} from "@/utils/utils";
|
|
|
import { dragImage } from "@/features/mark/use/draggable";
|
|
|
|
|
|
interface SliceImage {
|
|
@@ -44,6 +51,11 @@ interface SliceImage {
|
|
|
trackList: Array<Track>;
|
|
|
tagList: Array<SpecialTag>;
|
|
|
originalImage: HTMLImageElement;
|
|
|
+ sliceImage: HTMLImageElement;
|
|
|
+ dx: number;
|
|
|
+ dy: number;
|
|
|
+ accumTopHeight: number;
|
|
|
+ effectiveWidth: number;
|
|
|
}
|
|
|
// should not render twice at the same time
|
|
|
let __lock = false;
|
|
@@ -57,29 +69,74 @@ export default defineComponent({
|
|
|
|
|
|
const { addTimeout } = useTimers();
|
|
|
|
|
|
+ function hasSliceConfig() {
|
|
|
+ return store.currentTask?.sliceConfig?.length;
|
|
|
+ }
|
|
|
+
|
|
|
let rendering = ref(false);
|
|
|
let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
|
|
|
+ let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
|
|
|
+ let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
|
|
|
+
|
|
|
+ async function getImageUsingDataUrl(
|
|
|
+ dataUrl: string
|
|
|
+ ): Promise<HTMLImageElement> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.src = dataUrl;
|
|
|
+ image.onload = function () {
|
|
|
+ resolve(image);
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- async function processImage() {
|
|
|
+ async function processSliceConfig() {
|
|
|
if (!store.currentTask) return;
|
|
|
|
|
|
const images = [];
|
|
|
- for (const url of store.currentTask.sliceUrls) {
|
|
|
- const image = await loadImage(
|
|
|
- filters.toCompleteUrlWithFileServer(store.setting.fileServer, url)
|
|
|
+ const urls = [];
|
|
|
+ // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
|
|
|
+ for (const sliceConfig of store.currentTask.sliceConfig) {
|
|
|
+ const url = filters.toCompleteUrlWithFileServer(
|
|
|
+ store.setting.fileServer,
|
|
|
+ store.currentTask.sliceUrls[sliceConfig.i - 1]
|
|
|
);
|
|
|
+ const image = await loadImage(url);
|
|
|
images.push(image);
|
|
|
+ urls.push(url);
|
|
|
+ if (sliceConfig.w === 0 && sliceConfig.h === 0) {
|
|
|
+ // 选择整图时,w/h 为0
|
|
|
+ sliceConfig.w = image.naturalWidth;
|
|
|
+ sliceConfig.h = image.naturalHeight;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- for (const url of store.currentTask.sliceUrls) {
|
|
|
- const completeUrl = filters.toCompleteUrlWithFileServer(
|
|
|
+ theFinalHeight = store.currentTask.sliceConfig
|
|
|
+ .map((v) => v.h)
|
|
|
+ .reduce((acc, v) => (acc += v));
|
|
|
+ maxSliceWidth = Math.max(
|
|
|
+ ...store.currentTask.sliceConfig.map((v) => v.w)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
|
|
|
+ let accumTopHeight = 0;
|
|
|
+ let accumBottomHeight = 0;
|
|
|
+ const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
|
|
|
+ for (const sliceConfig of store.currentTask.sliceConfig) {
|
|
|
+ accumBottomHeight += sliceConfig.h;
|
|
|
+ const url = filters.toCompleteUrlWithFileServer(
|
|
|
store.setting.fileServer,
|
|
|
- url
|
|
|
+ store.currentTask.sliceUrls[sliceConfig.i - 1]
|
|
|
);
|
|
|
-
|
|
|
- const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
|
|
|
+ const indexInSliceUrls = sliceConfig.i;
|
|
|
const image = images[indexInSliceUrls - 1];
|
|
|
|
|
|
+ const dataUrl = getDataUrlForSliceConfig(
|
|
|
+ image,
|
|
|
+ sliceConfig,
|
|
|
+ maxSliceWidth,
|
|
|
+ url
|
|
|
+ );
|
|
|
const trackLists = store.currentTask.questionList
|
|
|
.map((q) => q.trackList)
|
|
|
.reduce((acc, t) => {
|
|
@@ -89,18 +146,114 @@ export default defineComponent({
|
|
|
const thisImageTrackList = trackLists.filter(
|
|
|
(t) => t.offsetIndex === indexInSliceUrls
|
|
|
);
|
|
|
- const thisImageTagList = store.currentTask.specialTagList?.filter(
|
|
|
- (t) => t.offsetIndex === indexInSliceUrls
|
|
|
- );
|
|
|
+ const thisImageTagList = (
|
|
|
+ store.currentTask.specialTagList ?? []
|
|
|
+ ).filter((t) => t.offsetIndex === indexInSliceUrls);
|
|
|
|
|
|
- sliceImagesWithTrackList.push({
|
|
|
- url: completeUrl,
|
|
|
+ const sliceImage = await getImageUsingDataUrl(dataUrl);
|
|
|
+ tempSliceImagesWithTrackList.push({
|
|
|
+ url: dataUrl,
|
|
|
indexInSliceUrls,
|
|
|
- trackList: thisImageTrackList,
|
|
|
- tagList: thisImageTagList,
|
|
|
+ // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
|
|
|
+ trackList: thisImageTrackList.filter(
|
|
|
+ (t) =>
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight &&
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight
|
|
|
+ ),
|
|
|
+ tagList: thisImageTagList.filter(
|
|
|
+ (t) =>
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight &&
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight
|
|
|
+ ),
|
|
|
originalImage: image,
|
|
|
+ sliceImage,
|
|
|
+ dx: sliceConfig.x,
|
|
|
+ dy: sliceConfig.y,
|
|
|
+ accumTopHeight,
|
|
|
+ effectiveWidth: sliceConfig.w,
|
|
|
});
|
|
|
+ accumTopHeight = accumBottomHeight;
|
|
|
+ }
|
|
|
+ sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
|
|
|
+ }
|
|
|
+
|
|
|
+ async function processSplitConfig() {
|
|
|
+ if (!store.currentTask) return;
|
|
|
+
|
|
|
+ const images = [];
|
|
|
+ for (const url of store.currentTask.sliceUrls) {
|
|
|
+ const image = await loadImage(
|
|
|
+ filters.toCompleteUrlWithFileServer(store.setting.fileServer, url)
|
|
|
+ );
|
|
|
+ images.push(image);
|
|
|
}
|
|
|
+
|
|
|
+ const splitConfigPairs = store.setting.splitConfig
|
|
|
+ .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
|
|
|
+ .filter((v) => v) as unknown as Array<[number, number]>;
|
|
|
+
|
|
|
+ const maxSplitConfig = Math.max(...store.setting.splitConfig);
|
|
|
+ maxSliceWidth =
|
|
|
+ Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
|
|
|
+
|
|
|
+ theFinalHeight =
|
|
|
+ splitConfigPairs.length *
|
|
|
+ images.reduce((acc, v) => (acc += v.naturalHeight), 0);
|
|
|
+
|
|
|
+ let accumTopHeight = 0;
|
|
|
+ let accumBottomHeight = 0;
|
|
|
+ const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
|
|
|
+ for (const url of store.currentTask.sliceUrls) {
|
|
|
+ for (const config of splitConfigPairs) {
|
|
|
+ const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
|
|
|
+ const image = images[indexInSliceUrls - 1];
|
|
|
+
|
|
|
+ accumBottomHeight += image.naturalHeight;
|
|
|
+
|
|
|
+ const dataUrl = getDataUrlForSplitConfig(
|
|
|
+ image,
|
|
|
+ config,
|
|
|
+ maxSliceWidth,
|
|
|
+ url
|
|
|
+ );
|
|
|
+
|
|
|
+ const trackLists = store.currentTask.questionList
|
|
|
+ .map((q) => q.trackList)
|
|
|
+ .reduce((acc, t) => {
|
|
|
+ acc = acc.concat(t);
|
|
|
+ return acc;
|
|
|
+ }, [] as Array<Track>);
|
|
|
+ const thisImageTrackList = trackLists.filter(
|
|
|
+ (t) => t.offsetIndex === indexInSliceUrls
|
|
|
+ );
|
|
|
+ const thisImageTagList = (
|
|
|
+ store.currentTask.specialTagList ?? []
|
|
|
+ ).filter((t) => t.offsetIndex === indexInSliceUrls);
|
|
|
+ const sliceImage = await getImageUsingDataUrl(dataUrl);
|
|
|
+ tempSliceImagesWithTrackList.push({
|
|
|
+ url: dataUrl,
|
|
|
+ indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
|
|
|
+ trackList: thisImageTrackList.filter(
|
|
|
+ (t) =>
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight &&
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight
|
|
|
+ ),
|
|
|
+ tagList: thisImageTagList.filter(
|
|
|
+ (t) =>
|
|
|
+ t.positionY >= accumTopHeight / theFinalHeight &&
|
|
|
+ t.positionY < accumBottomHeight / theFinalHeight
|
|
|
+ ),
|
|
|
+ originalImage: image,
|
|
|
+ sliceImage,
|
|
|
+ dx: image.naturalWidth * config[0],
|
|
|
+ dy: 0,
|
|
|
+ accumTopHeight,
|
|
|
+ effectiveWidth: image.naturalWidth * config[1],
|
|
|
+ });
|
|
|
+ accumTopHeight = accumBottomHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
|
|
|
}
|
|
|
const renderPaperAndMark = async () => {
|
|
|
if (__lock) {
|
|
@@ -124,7 +277,11 @@ export default defineComponent({
|
|
|
|
|
|
try {
|
|
|
rendering.value = true;
|
|
|
- await processImage();
|
|
|
+ if (hasSliceConfig()) {
|
|
|
+ await processSliceConfig();
|
|
|
+ } else {
|
|
|
+ await processSplitConfig();
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
sliceImagesWithTrackList.splice(0);
|
|
|
console.log("render error ", error);
|
|
@@ -182,12 +339,6 @@ export default defineComponent({
|
|
|
background-size: 8px 8px;
|
|
|
background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
|
|
|
linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
|
|
|
-
|
|
|
- cursor: grab;
|
|
|
- user-select: none;
|
|
|
-}
|
|
|
-.grabbing {
|
|
|
- cursor: grabbing;
|
|
|
}
|
|
|
.mark-body-container img {
|
|
|
width: 100%;
|