MarkBody.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. <template>
  2. <div class="mark-body-container tw-flex-auto tw-p-2" ref="dragContainer">
  3. <a-spin
  4. :spinning="rendering"
  5. size="large"
  6. tip="Loading..."
  7. style="margin-top: 50px"
  8. >
  9. <div v-if="!store.currentTask" class="tw-text-center">
  10. {{ store.message }}
  11. </div>
  12. <div v-else :style="{ width: answerPaperScale }">
  13. <div
  14. style="
  15. top: -10px;
  16. right: 0;
  17. position: absolute;
  18. color: red;
  19. pointer-events: none;
  20. font-size: 30px;
  21. z-index: 1000;
  22. "
  23. >
  24. <!-- @mouseover="(e) => (e.target.style.opacity = 0.01)"
  25. @mouseout="(e) => (e.target.style.opacity = 1)" -->
  26. {{ markStatus }}
  27. </div>
  28. <div
  29. v-for="(item, index) in sliceImagesWithTrackList"
  30. :key="index"
  31. class="single-image-container"
  32. >
  33. <img
  34. :src="item.url"
  35. @click="(event) => makeTrack(event, item)"
  36. draggable="false"
  37. />
  38. <MarkDrawTrack
  39. :track-list="item.trackList"
  40. :special-tag-list="item.tagList"
  41. :original-image="item.originalImage"
  42. :slice-image="item.sliceImage"
  43. :dx="item.dx"
  44. :dy="item.dy"
  45. />
  46. <hr class="image-seperator" />
  47. </div>
  48. </div>
  49. </a-spin>
  50. </div>
  51. <div class="cursor">
  52. <div class="cursor-border">
  53. <span class="text">{{
  54. store.currentSpecialTag || store.currentScore
  55. }}</span>
  56. </div>
  57. </div>
  58. </template>
  59. <script lang="ts">
  60. import {
  61. computed,
  62. defineComponent,
  63. onMounted,
  64. onUnmounted,
  65. reactive,
  66. ref,
  67. watch,
  68. watchEffect,
  69. } from "vue";
  70. import { getMarkStatus, store } from "./store";
  71. import filters from "@/filters";
  72. import MarkDrawTrack from "./MarkDrawTrack.vue";
  73. import { ModeEnum, SpecialTag, Track } from "@/types";
  74. import { useTimers } from "@/setups/useTimers";
  75. import {
  76. getDataUrlForSliceConfig,
  77. getDataUrlForSplitConfig,
  78. loadImage,
  79. } from "@/utils/utils";
  80. import { isNumber } from "lodash";
  81. // @ts-ignore
  82. import CustomCursor from "custom-cursor.js";
  83. import { dragImage } from "./use/draggable";
  84. interface SliceImage {
  85. url: string;
  86. indexInSliceUrls: number;
  87. trackList: Array<Track>;
  88. tagList: Array<SpecialTag>;
  89. originalImage: HTMLImageElement;
  90. sliceImage: HTMLImageElement;
  91. dx: number;
  92. dy: number;
  93. accumTopHeight: number;
  94. effectiveWidth: number;
  95. }
  96. // should not render twice at the same time
  97. let __lock = false;
  98. let __currentLibraryId = -1; // save __currentLibraryId of lock
  99. export default defineComponent({
  100. name: "MarkBody",
  101. components: { MarkDrawTrack },
  102. emits: ["error"],
  103. setup(props, { emit }) {
  104. const { dragContainer } = dragImage();
  105. const { addTimeout } = useTimers();
  106. function hasSliceConfig() {
  107. return store.currentTask?.sliceConfig?.length;
  108. }
  109. let rendering = ref(false);
  110. let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
  111. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  112. let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
  113. async function getImageUsingDataUrl(
  114. dataUrl: string
  115. ): Promise<HTMLImageElement> {
  116. return new Promise((resolve) => {
  117. const image = new Image();
  118. image.src = dataUrl;
  119. image.onload = function () {
  120. resolve(image);
  121. };
  122. });
  123. }
  124. async function processSliceConfig() {
  125. // check if have MarkResult for currentTask
  126. let markResult = store.currentMarkResult;
  127. if (!markResult || !store.currentTask) return;
  128. const images = [];
  129. const urls = [];
  130. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  131. for (const sliceConfig of store.currentTask.sliceConfig) {
  132. const url = filters.toCompleteUrl(
  133. store.currentTask.sliceUrls[sliceConfig.i - 1]
  134. );
  135. const image = await loadImage(url);
  136. images.push(image);
  137. urls.push(url);
  138. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  139. // 选择整图时,w/h 为0
  140. sliceConfig.w = image.naturalWidth;
  141. sliceConfig.h = image.naturalHeight;
  142. }
  143. }
  144. theFinalHeight = store.currentTask.sliceConfig
  145. .map((v) => v.h)
  146. .reduce((acc, v) => (acc += v));
  147. maxSliceWidth = Math.max(
  148. ...store.currentTask.sliceConfig.map((v) => v.w)
  149. );
  150. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  151. let accumTopHeight = 0;
  152. let accumBottomHeight = 0;
  153. const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
  154. for (const sliceConfig of store.currentTask.sliceConfig) {
  155. accumBottomHeight += sliceConfig.h;
  156. const url = filters.toCompleteUrl(
  157. store.currentTask.sliceUrls[sliceConfig.i - 1]
  158. );
  159. const image = images[urls.indexOf(url)];
  160. const dataUrl = getDataUrlForSliceConfig(
  161. image,
  162. sliceConfig,
  163. maxSliceWidth,
  164. url
  165. );
  166. const thisImageTrackList = markResult.trackList.filter(
  167. (v) => v.offsetIndex === sliceConfig.i
  168. );
  169. const thisImageTagList = markResult.specialTagList.filter(
  170. (v) => v.offsetIndex === sliceConfig.i
  171. );
  172. const sliceImage = await getImageUsingDataUrl(dataUrl);
  173. tempSliceImagesWithTrackList.push({
  174. url: dataUrl,
  175. indexInSliceUrls: sliceConfig.i,
  176. // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
  177. trackList: thisImageTrackList.filter(
  178. (t) =>
  179. t.positionY >= accumTopHeight / theFinalHeight &&
  180. t.positionY < accumBottomHeight / theFinalHeight
  181. ),
  182. tagList: thisImageTagList.filter(
  183. (t) =>
  184. t.positionY >= accumTopHeight / theFinalHeight &&
  185. t.positionY < accumBottomHeight / theFinalHeight
  186. ),
  187. originalImage: image,
  188. sliceImage,
  189. dx: sliceConfig.x,
  190. dy: sliceConfig.y,
  191. accumTopHeight,
  192. effectiveWidth: sliceConfig.w,
  193. });
  194. accumTopHeight = accumBottomHeight;
  195. }
  196. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  197. }
  198. async function processSplitConfig() {
  199. // check if have MarkResult for currentTask
  200. let markResult = store.currentMarkResult;
  201. if (!markResult || !store.currentTask) return;
  202. const images = [];
  203. for (const url of store.currentTask.sliceUrls) {
  204. const image = await loadImage(filters.toCompleteUrl(url));
  205. images.push(image);
  206. }
  207. const splitConfigPairs = (store.setting.splitConfig
  208. .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
  209. .filter((v) => v) as unknown) as Array<[number, number]>;
  210. const maxSplitConfig = Math.max(...store.setting.splitConfig);
  211. maxSliceWidth =
  212. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  213. theFinalHeight =
  214. splitConfigPairs.length *
  215. images.reduce((acc, v) => (acc += v.naturalHeight), 0);
  216. let accumTopHeight = 0;
  217. let accumBottomHeight = 0;
  218. const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
  219. for (const url of store.currentTask.sliceUrls) {
  220. for (const config of splitConfigPairs) {
  221. const image = images[store.currentTask.sliceUrls.indexOf(url)];
  222. accumBottomHeight += image.naturalHeight;
  223. const dataUrl = getDataUrlForSplitConfig(
  224. image,
  225. config,
  226. maxSliceWidth,
  227. url
  228. );
  229. const thisImageTrackList = markResult.trackList.filter(
  230. (t) =>
  231. t.offsetIndex ===
  232. (store.currentTask &&
  233. store.currentTask.sliceUrls.indexOf(url) + 1)
  234. );
  235. const thisImageTagList = markResult.specialTagList.filter(
  236. (t) =>
  237. t.offsetIndex ===
  238. (store.currentTask &&
  239. store.currentTask.sliceUrls.indexOf(url) + 1)
  240. );
  241. const sliceImage = await getImageUsingDataUrl(dataUrl);
  242. tempSliceImagesWithTrackList.push({
  243. url: dataUrl,
  244. indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
  245. trackList: thisImageTrackList.filter(
  246. (t) =>
  247. t.positionY >= accumTopHeight / theFinalHeight &&
  248. t.positionY < accumBottomHeight / theFinalHeight
  249. ),
  250. tagList: thisImageTagList.filter(
  251. (t) =>
  252. t.positionY >= accumTopHeight / theFinalHeight &&
  253. t.positionY < accumBottomHeight / theFinalHeight
  254. ),
  255. originalImage: image,
  256. sliceImage,
  257. dx: image.naturalWidth * config[0],
  258. dy: 0,
  259. accumTopHeight,
  260. effectiveWidth: image.naturalWidth * config[1],
  261. });
  262. accumTopHeight = accumBottomHeight;
  263. }
  264. }
  265. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  266. }
  267. const renderPaperAndMark = async () => {
  268. if (__lock) {
  269. if (store.currentTask?.libraryId === __currentLibraryId) {
  270. console.log("重复渲染,返回");
  271. return;
  272. }
  273. console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
  274. await new Promise((res) => setTimeout(res, 1000));
  275. await renderPaperAndMark();
  276. return;
  277. }
  278. __lock = true;
  279. __currentLibraryId = store.currentTask?.libraryId ?? -1;
  280. sliceImagesWithTrackList.splice(0);
  281. // check if have MarkResult for currentTask
  282. let markResult = store.currentMarkResult;
  283. if (!markResult || !store.currentTask) {
  284. __lock = false;
  285. return;
  286. }
  287. try {
  288. rendering.value = true;
  289. if (hasSliceConfig()) {
  290. await processSliceConfig();
  291. } else {
  292. await processSplitConfig();
  293. }
  294. } catch (error) {
  295. sliceImagesWithTrackList.splice(0);
  296. console.log("render error ", error);
  297. // 图片加载出错,自动加载下一个任务
  298. emit("error");
  299. } finally {
  300. __lock = false;
  301. rendering.value = false;
  302. }
  303. };
  304. watchEffect(renderPaperAndMark);
  305. watch(
  306. () => store.minimapScrollTo,
  307. () => {
  308. const container = document.querySelector(
  309. ".mark-body-container"
  310. ) as HTMLDivElement;
  311. addTimeout(() => {
  312. if (container) {
  313. const { scrollHeight } = container;
  314. container.scrollTo({
  315. top: scrollHeight * store.minimapScrollTo,
  316. });
  317. }
  318. }, 10);
  319. }
  320. );
  321. const answerPaperScale = computed(() => {
  322. // 放大、缩小不影响页面之前的滚动条定位
  323. let percentWidth = 0;
  324. let percentTop = 0;
  325. const container = document.querySelector(
  326. ".mark-body-container"
  327. ) as HTMLDivElement;
  328. if (container) {
  329. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  330. percentWidth = scrollLeft / scrollWidth;
  331. percentTop = scrollTop / scrollHeight;
  332. }
  333. addTimeout(() => {
  334. if (container) {
  335. const { scrollWidth, scrollHeight } = container;
  336. container.scrollTo({
  337. left: scrollWidth * percentWidth,
  338. top: scrollHeight * percentTop,
  339. });
  340. }
  341. }, 10);
  342. const scale = store.setting.uiSetting["answer.paper.scale"];
  343. return scale * 100 + "%";
  344. });
  345. const makeScoreTrack = (event: MouseEvent, item: SliceImage) => {
  346. // console.log(item);
  347. if (!store.currentQuestion || typeof store.currentScore === "undefined")
  348. return;
  349. const target = event.target as HTMLImageElement;
  350. const track = {} as Track;
  351. track.mainNumber = store.currentQuestion?.mainNumber;
  352. track.subNumber = store.currentQuestion?.subNumber;
  353. // track.number = (Date.now() - new Date(2021, 0, 0).valueOf()) / 10e7;
  354. track.score = store.currentScore;
  355. track.offsetIndex = item.indexInSliceUrls;
  356. track.offsetX = Math.round(
  357. event.offsetX * (target.naturalWidth / target.width) + item.dx
  358. );
  359. track.offsetY = Math.round(
  360. event.offsetY * (target.naturalHeight / target.height) + item.dy
  361. );
  362. track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
  363. track.positionY =
  364. (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
  365. if (track.offsetX > item.effectiveWidth + item.dx) {
  366. console.log("不在有效宽度内,轨迹不生效");
  367. return;
  368. }
  369. if (
  370. item.trackList.some((t) => {
  371. return (
  372. Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
  373. Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
  374. 500
  375. );
  376. })
  377. ) {
  378. console.log("两个轨迹相距过近");
  379. return;
  380. }
  381. // 是否保留当前的轨迹分
  382. const ifKeepScore =
  383. Math.round(
  384. store.currentQuestion.maxScore * 100 -
  385. (store.currentQuestion.score || 0) * 100 -
  386. store.currentScore * 2 * 100
  387. ) / 100;
  388. if (
  389. (ifKeepScore < 0 && store.currentScore > 0) ||
  390. Math.round(ifKeepScore * 100) %
  391. Math.round(store.currentQuestion.intervalScore * 100) !==
  392. 0
  393. ) {
  394. store.currentScore = undefined;
  395. }
  396. const markResult = store.currentMarkResult;
  397. if (markResult) {
  398. const maxNumber =
  399. markResult.trackList.length === 0
  400. ? 0
  401. : Math.max(...markResult.trackList.map((t) => t.number));
  402. track.number = maxNumber + 1;
  403. // console.log(
  404. // maxNumber,
  405. // track.number,
  406. // markResult.trackList.map((t) => t.number),
  407. // Math.max(...markResult.trackList.map((t) => t.number))
  408. // );
  409. markResult.trackList = [...markResult.trackList, track];
  410. }
  411. item.trackList.push(track);
  412. };
  413. const makeSpecialTagTrack = (event: MouseEvent, item: SliceImage) => {
  414. // console.log(item);
  415. if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
  416. return;
  417. const target = event.target as HTMLImageElement;
  418. const track = {} as SpecialTag;
  419. track.tagName = store.currentSpecialTag;
  420. track.offsetIndex = item.indexInSliceUrls;
  421. track.offsetX = Math.round(
  422. event.offsetX * (target.naturalWidth / target.width) + item.dx
  423. );
  424. track.offsetY = Math.round(
  425. event.offsetY * (target.naturalHeight / target.height) + item.dy
  426. );
  427. track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
  428. track.positionY =
  429. (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
  430. if (track.offsetX > item.effectiveWidth + item.dx) {
  431. console.log("不在有效宽度内,轨迹不生效");
  432. return;
  433. }
  434. if (
  435. item.tagList.some((t) => {
  436. return (
  437. Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
  438. Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
  439. 500
  440. );
  441. })
  442. ) {
  443. console.log("两个轨迹相距过近");
  444. return;
  445. }
  446. const markResult = store.currentMarkResult;
  447. if (markResult) {
  448. markResult.specialTagList.push(track);
  449. }
  450. item.tagList.push(track);
  451. };
  452. const makeTrack = (event: MouseEvent, item: SliceImage) => {
  453. if (
  454. store.setting.uiSetting["specialTag.modal"] &&
  455. store.currentSpecialTag
  456. ) {
  457. makeSpecialTagTrack(event, item);
  458. } else {
  459. makeScoreTrack(event, item);
  460. }
  461. };
  462. // 清除分数轨迹
  463. watchEffect(() => {
  464. for (const track of store.removeScoreTracks) {
  465. for (const sliceImage of sliceImagesWithTrackList) {
  466. sliceImage.trackList = sliceImage.trackList.filter(
  467. (t) =>
  468. !(
  469. t.mainNumber === track.mainNumber &&
  470. t.subNumber === track.subNumber &&
  471. t.number === track.number
  472. )
  473. );
  474. }
  475. }
  476. // 清除后,删除,否则会影响下次切换
  477. store.removeScoreTracks.splice(0);
  478. });
  479. // 清除特殊标记轨迹
  480. watchEffect(() => {
  481. for (const track of store.currentMarkResult?.specialTagList || []) {
  482. for (const sliceImage of sliceImagesWithTrackList) {
  483. sliceImage.tagList = sliceImage.tagList.filter((t) =>
  484. store.currentMarkResult?.specialTagList.find(
  485. (st) =>
  486. st.offsetIndex === t.offsetIndex &&
  487. st.offsetX === t.offsetX &&
  488. st.offsetY === t.offsetY
  489. )
  490. );
  491. }
  492. }
  493. if (store.currentMarkResult?.specialTagList.length === 0) {
  494. for (const sliceImage of sliceImagesWithTrackList) {
  495. sliceImage.tagList = [];
  496. }
  497. }
  498. });
  499. // 轨迹模式下,添加轨迹,更新分数
  500. watch(
  501. () => store.currentMarkResult?.trackList,
  502. () => {
  503. if (store.setting.mode !== ModeEnum.TRACK) return;
  504. const markResult = store.currentMarkResult;
  505. if (markResult) {
  506. const cq = store.currentQuestion;
  507. // 当无轨迹时,不更新;无轨迹时,将分数置null
  508. if (cq) {
  509. if (markResult.trackList.length > 0) {
  510. const cqTrackList = markResult.trackList.filter(
  511. (v) =>
  512. v.mainNumber === cq.mainNumber && v.subNumber === cq.subNumber
  513. );
  514. if (cqTrackList.length > 0) {
  515. cq.score =
  516. cqTrackList
  517. .map((v) => v.score)
  518. .reduce((acc, v) => (acc += Math.round(v * 100)), 0) / 100;
  519. } else {
  520. cq.score = null;
  521. }
  522. } else {
  523. // TODO: 不需要?如果此行代码生效,则无法清除最后一道题的分数 此时的场景是回评普通模式评的分,需要看见
  524. // cq.score = cq.__origScore;
  525. }
  526. }
  527. // renderPaperAndMark();
  528. }
  529. },
  530. { deep: true }
  531. );
  532. // question.score更新后,自动关联markResult.scoreList和markResult.markerScore
  533. watchEffect(() => {
  534. const markResult = store.currentMarkResult;
  535. if (markResult && store.currentTask) {
  536. const scoreList = store.currentTask.questionList.map((q) => q.score);
  537. markResult.scoreList = [...(scoreList as number[])];
  538. markResult.markerScore =
  539. (markResult.scoreList.filter((s) => isNumber(s)) as number[]).reduce(
  540. (acc, v) => (acc += Math.round(v * 100)),
  541. 0
  542. ) / 100;
  543. }
  544. });
  545. watch(
  546. () => store.setting.mode,
  547. () => {
  548. const shouldHide = store.setting.mode === ModeEnum.COMMON;
  549. if (shouldHide) {
  550. // console.log("hide cursor", theCursor);
  551. theCursor && theCursor.destroy();
  552. } else {
  553. if (document.querySelector(".cursor")) {
  554. // console.log("show cursor", theCursor);
  555. // theCursor && theCursor.enable();
  556. theCursor = new CustomCursor(".cursor", {
  557. focusElements: [
  558. {
  559. selector: ".mark-body-container",
  560. focusClass: "cursor--focused-view",
  561. },
  562. ],
  563. }).initialize();
  564. }
  565. }
  566. }
  567. );
  568. let theCursor = null as any;
  569. onMounted(() => {
  570. if (store.setting.mode === ModeEnum.TRACK) {
  571. theCursor = new CustomCursor(".cursor", {
  572. focusElements: [
  573. {
  574. selector: ".mark-body-container",
  575. focusClass: "cursor--focused-view",
  576. },
  577. ],
  578. }).initialize();
  579. }
  580. });
  581. onUnmounted(() => {
  582. theCursor && theCursor.destroy();
  583. });
  584. const markStatus = ref("");
  585. watch(
  586. () => store.currentTask,
  587. () => {
  588. markStatus.value = getMarkStatus();
  589. }
  590. );
  591. return {
  592. dragContainer,
  593. store,
  594. rendering,
  595. sliceImagesWithTrackList,
  596. answerPaperScale,
  597. makeTrack,
  598. markStatus,
  599. };
  600. },
  601. // renderTriggered({ key, target, type }) {
  602. // console.log({ key, target, type });
  603. // },
  604. });
  605. </script>
  606. <style scoped>
  607. .mark-body-container {
  608. height: calc(100vh - 41px);
  609. overflow: scroll;
  610. background-size: 8px 8px;
  611. background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
  612. linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
  613. }
  614. .mark-body-container img {
  615. width: 100%;
  616. }
  617. .single-image-container {
  618. position: relative;
  619. }
  620. .image-seperator {
  621. border: 2px solid rgba(120, 120, 120, 0.1);
  622. }
  623. .hide-cursor {
  624. display: none !important;
  625. }
  626. .cursor {
  627. color: #ff5050;
  628. display: none;
  629. pointer-events: none;
  630. -webkit-user-select: none;
  631. -moz-user-select: none;
  632. -ms-user-select: none;
  633. user-select: none;
  634. top: 0;
  635. left: 0;
  636. position: fixed;
  637. will-change: transform;
  638. z-index: 1000;
  639. }
  640. .cursor-border {
  641. position: absolute;
  642. box-sizing: border-box;
  643. align-items: center;
  644. border: 1px solid #ff5050;
  645. border-radius: 50%;
  646. display: flex;
  647. justify-content: center;
  648. height: 0px;
  649. width: 0px;
  650. left: 0;
  651. top: 0;
  652. transform: translate(-50%, -50%);
  653. transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
  654. }
  655. .cursor.cursor--initialized {
  656. display: block;
  657. }
  658. .cursor .text {
  659. font-size: 2rem;
  660. opacity: 0;
  661. transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
  662. }
  663. .cursor.cursor--off-screen {
  664. opacity: 0;
  665. }
  666. .cursor.cursor--focused .cursor-border,
  667. .cursor.cursor--focused-view .cursor-border {
  668. width: 90px;
  669. height: 90px;
  670. }
  671. .cursor.cursor--focused-view .text {
  672. opacity: 1;
  673. transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
  674. }
  675. </style>