MarkBody.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  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. // TODO: add loading
  208. const splitConfigPairs = (store.setting.splitConfig
  209. .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
  210. .filter((v) => v) as unknown) as Array<[number, number]>;
  211. const maxSplitConfig = Math.max(...store.setting.splitConfig);
  212. maxSliceWidth =
  213. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  214. theFinalHeight =
  215. splitConfigPairs.length *
  216. images.reduce((acc, v) => (acc += v.naturalHeight), 0);
  217. let accumTopHeight = 0;
  218. let accumBottomHeight = 0;
  219. const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
  220. for (const url of store.currentTask.sliceUrls) {
  221. for (const config of splitConfigPairs) {
  222. const image = images[store.currentTask.sliceUrls.indexOf(url)];
  223. accumBottomHeight += image.naturalHeight;
  224. const dataUrl = getDataUrlForSplitConfig(
  225. image,
  226. config,
  227. maxSliceWidth,
  228. url
  229. );
  230. const thisImageTrackList = markResult.trackList.filter(
  231. (t) =>
  232. t.offsetIndex ===
  233. (store.currentTask &&
  234. store.currentTask.sliceUrls.indexOf(url) + 1)
  235. );
  236. const thisImageTagList = markResult.specialTagList.filter(
  237. (t) =>
  238. t.offsetIndex ===
  239. (store.currentTask &&
  240. store.currentTask.sliceUrls.indexOf(url) + 1)
  241. );
  242. const sliceImage = await getImageUsingDataUrl(dataUrl);
  243. tempSliceImagesWithTrackList.push({
  244. url: dataUrl,
  245. indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
  246. trackList: thisImageTrackList.filter(
  247. (t) =>
  248. t.positionY >= accumTopHeight / theFinalHeight &&
  249. t.positionY < accumBottomHeight / theFinalHeight
  250. ),
  251. tagList: thisImageTagList.filter(
  252. (t) =>
  253. t.positionY >= accumTopHeight / theFinalHeight &&
  254. t.positionY < accumBottomHeight / theFinalHeight
  255. ),
  256. originalImage: image,
  257. sliceImage,
  258. dx: image.naturalWidth * config[0],
  259. dy: 0,
  260. accumTopHeight,
  261. effectiveWidth: image.naturalWidth * config[1],
  262. });
  263. accumTopHeight = accumBottomHeight;
  264. }
  265. }
  266. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  267. }
  268. const renderPaperAndMark = async () => {
  269. if (__lock) {
  270. if (store.currentTask?.libraryId === __currentLibraryId) {
  271. console.log("重复渲染,返回");
  272. return;
  273. }
  274. console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
  275. await new Promise((res) => setTimeout(res, 1000));
  276. await renderPaperAndMark();
  277. return;
  278. }
  279. __lock = true;
  280. __currentLibraryId = store.currentTask?.libraryId ?? -1;
  281. sliceImagesWithTrackList.splice(0);
  282. // check if have MarkResult for currentTask
  283. let markResult = store.currentMarkResult;
  284. if (!markResult || !store.currentTask) {
  285. __lock = false;
  286. return;
  287. }
  288. try {
  289. rendering.value = true;
  290. if (hasSliceConfig()) {
  291. await processSliceConfig();
  292. } else {
  293. await processSplitConfig();
  294. }
  295. } catch (error) {
  296. sliceImagesWithTrackList.splice(0);
  297. console.log("render error ", error);
  298. // 图片加载出错,自动加载下一个任务
  299. emit("error");
  300. } finally {
  301. __lock = false;
  302. rendering.value = false;
  303. }
  304. };
  305. watchEffect(renderPaperAndMark);
  306. watch(
  307. () => store.minimapScrollTo,
  308. () => {
  309. const container = document.querySelector(
  310. ".mark-body-container"
  311. ) as HTMLDivElement;
  312. addTimeout(() => {
  313. if (container) {
  314. const { scrollHeight } = container;
  315. container.scrollTo({
  316. top: scrollHeight * store.minimapScrollTo,
  317. });
  318. }
  319. }, 10);
  320. }
  321. );
  322. const answerPaperScale = computed(() => {
  323. // 放大、缩小不影响页面之前的滚动条定位
  324. let percentWidth = 0;
  325. let percentTop = 0;
  326. const container = document.querySelector(
  327. ".mark-body-container"
  328. ) as HTMLDivElement;
  329. if (container) {
  330. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  331. percentWidth = scrollLeft / scrollWidth;
  332. percentTop = scrollTop / scrollHeight;
  333. }
  334. addTimeout(() => {
  335. if (container) {
  336. const { scrollWidth, scrollHeight } = container;
  337. container.scrollTo({
  338. left: scrollWidth * percentWidth,
  339. top: scrollHeight * percentTop,
  340. });
  341. }
  342. }, 10);
  343. const scale = store.setting.uiSetting["answer.paper.scale"];
  344. return scale * 100 + "%";
  345. });
  346. const makeScoreTrack = (event: MouseEvent, item: SliceImage) => {
  347. // console.log(item);
  348. if (!store.currentQuestion || typeof store.currentScore === "undefined")
  349. return;
  350. const target = event.target as HTMLImageElement;
  351. const track = {} as Track;
  352. track.mainNumber = store.currentQuestion?.mainNumber;
  353. track.subNumber = store.currentQuestion?.subNumber;
  354. // track.number = (Date.now() - new Date(2021, 0, 0).valueOf()) / 10e7;
  355. track.score = store.currentScore;
  356. track.offsetIndex = item.indexInSliceUrls;
  357. track.offsetX = Math.round(
  358. event.offsetX * (target.naturalWidth / target.width) + item.dx
  359. );
  360. track.offsetY = Math.round(
  361. event.offsetY * (target.naturalHeight / target.height) + item.dy
  362. );
  363. track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
  364. track.positionY =
  365. (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
  366. if (track.offsetX > item.effectiveWidth + item.dx) {
  367. console.log("不在有效宽度内,轨迹不生效");
  368. return;
  369. }
  370. if (
  371. item.trackList.some((t) => {
  372. return (
  373. Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
  374. Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
  375. 500
  376. );
  377. })
  378. ) {
  379. console.log("两个轨迹相距过近");
  380. return;
  381. }
  382. // 是否保留当前的轨迹分
  383. const ifKeepScore =
  384. Math.round(
  385. store.currentQuestion.maxScore * 100 -
  386. (store.currentQuestion.score || 0) * 100 -
  387. store.currentScore * 2 * 100
  388. ) / 100;
  389. if (
  390. (ifKeepScore < 0 && store.currentScore > 0) ||
  391. Math.round(ifKeepScore * 100) %
  392. Math.round(store.currentQuestion.intervalScore * 100) !==
  393. 0
  394. ) {
  395. store.currentScore = undefined;
  396. }
  397. const markResult = store.currentMarkResult;
  398. if (markResult) {
  399. const maxNumber =
  400. markResult.trackList.length === 0
  401. ? 0
  402. : Math.max(...markResult.trackList.map((t) => t.number));
  403. track.number = maxNumber + 1;
  404. // console.log(
  405. // maxNumber,
  406. // track.number,
  407. // markResult.trackList.map((t) => t.number),
  408. // Math.max(...markResult.trackList.map((t) => t.number))
  409. // );
  410. markResult.trackList = [...markResult.trackList, track];
  411. }
  412. item.trackList.push(track);
  413. };
  414. const makeSpecialTagTrack = (event: MouseEvent, item: SliceImage) => {
  415. // console.log(item);
  416. if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
  417. return;
  418. const target = event.target as HTMLImageElement;
  419. const track = {} as SpecialTag;
  420. track.tagName = store.currentSpecialTag;
  421. track.offsetIndex = item.indexInSliceUrls;
  422. track.offsetX = Math.round(
  423. event.offsetX * (target.naturalWidth / target.width) + item.dx
  424. );
  425. track.offsetY = Math.round(
  426. event.offsetY * (target.naturalHeight / target.height) + item.dy
  427. );
  428. track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
  429. track.positionY =
  430. (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
  431. if (track.offsetX > item.effectiveWidth + item.dx) {
  432. console.log("不在有效宽度内,轨迹不生效");
  433. return;
  434. }
  435. if (
  436. item.tagList.some((t) => {
  437. return (
  438. Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
  439. Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
  440. 500
  441. );
  442. })
  443. ) {
  444. console.log("两个轨迹相距过近");
  445. return;
  446. }
  447. const markResult = store.currentMarkResult;
  448. if (markResult) {
  449. markResult.specialTagList.push(track);
  450. }
  451. item.tagList.push(track);
  452. };
  453. const makeTrack = (event: MouseEvent, item: SliceImage) => {
  454. if (
  455. store.setting.uiSetting["specialTag.modal"] &&
  456. store.currentSpecialTag
  457. ) {
  458. makeSpecialTagTrack(event, item);
  459. } else {
  460. makeScoreTrack(event, item);
  461. }
  462. };
  463. // 清除分数轨迹
  464. watchEffect(() => {
  465. for (const track of store.removeScoreTracks) {
  466. for (const sliceImage of sliceImagesWithTrackList) {
  467. sliceImage.trackList = sliceImage.trackList.filter(
  468. (t) =>
  469. !(
  470. t.mainNumber === track.mainNumber &&
  471. t.subNumber === track.subNumber &&
  472. t.number === track.number
  473. )
  474. );
  475. }
  476. }
  477. // 清除后,删除,否则会影响下次切换
  478. store.removeScoreTracks.splice(0);
  479. });
  480. // 清除特殊标记轨迹
  481. watchEffect(() => {
  482. for (const track of store.currentMarkResult?.specialTagList || []) {
  483. for (const sliceImage of sliceImagesWithTrackList) {
  484. sliceImage.tagList = sliceImage.tagList.filter((t) =>
  485. store.currentMarkResult?.specialTagList.find(
  486. (st) =>
  487. st.offsetIndex === t.offsetIndex &&
  488. st.offsetX === t.offsetX &&
  489. st.offsetY === t.offsetY
  490. )
  491. );
  492. }
  493. }
  494. if (store.currentMarkResult?.specialTagList.length === 0) {
  495. for (const sliceImage of sliceImagesWithTrackList) {
  496. sliceImage.tagList = [];
  497. }
  498. }
  499. });
  500. // 轨迹模式下,添加轨迹,更新分数
  501. watch(
  502. () => store.currentMarkResult?.trackList,
  503. () => {
  504. if (store.setting.mode !== ModeEnum.TRACK) return;
  505. const markResult = store.currentMarkResult;
  506. if (markResult) {
  507. const cq = store.currentQuestion;
  508. // 当无轨迹时,不更新;无轨迹时,将分数置null
  509. if (cq) {
  510. if (markResult.trackList.length > 0) {
  511. const cqTrackList = markResult.trackList.filter(
  512. (v) =>
  513. v.mainNumber === cq.mainNumber && v.subNumber === cq.subNumber
  514. );
  515. if (cqTrackList.length > 0) {
  516. cq.score =
  517. cqTrackList
  518. .map((v) => v.score)
  519. .reduce((acc, v) => (acc += Math.round(v * 100)), 0) / 100;
  520. } else {
  521. cq.score = null;
  522. }
  523. } else {
  524. // TODO: 不需要?如果此行代码生效,则无法清除最后一道题的分数 此时的场景是回评普通模式评的分,需要看见
  525. // cq.score = cq.__origScore;
  526. }
  527. }
  528. // renderPaperAndMark();
  529. }
  530. },
  531. { deep: true }
  532. );
  533. // question.score更新后,自动关联markResult.scoreList和markResult.markerScore
  534. watchEffect(() => {
  535. const markResult = store.currentMarkResult;
  536. if (markResult && store.currentTask) {
  537. const scoreList = store.currentTask.questionList.map((q) => q.score);
  538. markResult.scoreList = [...(scoreList as number[])];
  539. markResult.markerScore =
  540. (markResult.scoreList.filter((s) => isNumber(s)) as number[]).reduce(
  541. (acc, v) => (acc += Math.round(v * 100)),
  542. 0
  543. ) / 100;
  544. }
  545. });
  546. watch(
  547. () => store.setting.mode,
  548. () => {
  549. const shouldHide = store.setting.mode === ModeEnum.COMMON;
  550. if (shouldHide) {
  551. // console.log("hide cursor", theCursor);
  552. theCursor && theCursor.destroy();
  553. } else {
  554. if (document.querySelector(".cursor")) {
  555. // console.log("show cursor", theCursor);
  556. // theCursor && theCursor.enable();
  557. theCursor = new CustomCursor(".cursor", {
  558. focusElements: [
  559. {
  560. selector: ".mark-body-container",
  561. focusClass: "cursor--focused-view",
  562. },
  563. ],
  564. }).initialize();
  565. }
  566. }
  567. }
  568. );
  569. let theCursor = null as any;
  570. onMounted(() => {
  571. if (store.setting.mode === ModeEnum.TRACK) {
  572. theCursor = new CustomCursor(".cursor", {
  573. focusElements: [
  574. {
  575. selector: ".mark-body-container",
  576. focusClass: "cursor--focused-view",
  577. },
  578. ],
  579. }).initialize();
  580. }
  581. });
  582. onUnmounted(() => {
  583. theCursor && theCursor.destroy();
  584. });
  585. const markStatus = ref("");
  586. watch(
  587. () => store.currentTask,
  588. () => {
  589. markStatus.value = getMarkStatus();
  590. }
  591. );
  592. return {
  593. dragContainer,
  594. store,
  595. rendering,
  596. sliceImagesWithTrackList,
  597. answerPaperScale,
  598. makeTrack,
  599. markStatus,
  600. };
  601. },
  602. // renderTriggered({ key, target, type }) {
  603. // console.log({ key, target, type });
  604. // },
  605. });
  606. </script>
  607. <style scoped>
  608. .mark-body-container {
  609. height: calc(100vh - 41px);
  610. overflow: scroll;
  611. background-size: 8px 8px;
  612. background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
  613. linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
  614. }
  615. .mark-body-container img {
  616. width: 100%;
  617. }
  618. .single-image-container {
  619. position: relative;
  620. }
  621. .image-seperator {
  622. border: 2px solid rgba(120, 120, 120, 0.1);
  623. }
  624. .hide-cursor {
  625. display: none !important;
  626. }
  627. .cursor {
  628. color: #ff5050;
  629. display: none;
  630. pointer-events: none;
  631. -webkit-user-select: none;
  632. -moz-user-select: none;
  633. -ms-user-select: none;
  634. user-select: none;
  635. top: 0;
  636. left: 0;
  637. position: fixed;
  638. will-change: transform;
  639. z-index: 1000;
  640. }
  641. .cursor-border {
  642. position: absolute;
  643. box-sizing: border-box;
  644. align-items: center;
  645. border: 1px solid #ff5050;
  646. border-radius: 50%;
  647. display: flex;
  648. justify-content: center;
  649. height: 0px;
  650. width: 0px;
  651. left: 0;
  652. top: 0;
  653. transform: translate(-50%, -50%);
  654. transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
  655. }
  656. .cursor.cursor--initialized {
  657. display: block;
  658. }
  659. .cursor .text {
  660. font-size: 2rem;
  661. opacity: 0;
  662. transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
  663. }
  664. .cursor.cursor--off-screen {
  665. opacity: 0;
  666. }
  667. .cursor.cursor--focused .cursor-border,
  668. .cursor.cursor--focused-view .cursor-border {
  669. width: 90px;
  670. height: 90px;
  671. }
  672. .cursor.cursor--focused-view .text {
  673. opacity: 1;
  674. transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
  675. }
  676. </style>