You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by ro...@apache.org on 2024/09/03 07:04:46 UTC

(incubator-answer) 02/02: feat: Add animation to badgeModal

This is an automated email from the ASF dual-hosted git repository.

robin0716 pushed a commit to branch test
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit daab3eebee5e4bd8776a9424ae5f651ed7c38cae
Author: robin <ro...@rnode.me>
AuthorDate: Tue Sep 3 15:03:28 2024 +0800

    feat: Add animation to badgeModal
---
 ui/src/components/Modal/BadgeModal.tsx |  40 ++++++-
 ui/src/components/Modal/Modal.tsx      |   3 +
 ui/src/utils/animateGift.ts            | 183 +++++++++++++++++++++++++++++++++
 ui/src/utils/progress.ts               |  77 ++++++++++++++
 4 files changed, 302 insertions(+), 1 deletion(-)

diff --git a/ui/src/components/Modal/BadgeModal.tsx b/ui/src/components/Modal/BadgeModal.tsx
index 468c7f89..303c9677 100644
--- a/ui/src/components/Modal/BadgeModal.tsx
+++ b/ui/src/components/Modal/BadgeModal.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { FC } from 'react';
+import { FC, useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
 
@@ -26,6 +26,7 @@ import classNames from 'classnames';
 import type * as Type from '@/common/interface';
 import { loggedUserInfoStore } from '@/stores';
 import { readNotification, useQueryNotificationStatus } from '@/services';
+import AnimateGift from '@/utils/animateGift';
 import Icon from '../Icon';
 
 import Modal from './Modal';
@@ -34,6 +35,10 @@ interface BadgeModalProps {
   badge?: Type.NotificationBadgeAward | null;
   visible: boolean;
 }
+
+let bg1: AnimateGift;
+let bg2: AnimateGift;
+let timeout: NodeJS.Timeout;
 const BadgeModal: FC<BadgeModalProps> = ({ badge, visible }) => {
   const { t } = useTranslation('translation', { keyPrefix: 'badges.modal' });
   const { user } = loggedUserInfoStore();
@@ -47,6 +52,9 @@ const BadgeModal: FC<BadgeModalProps> = ({ badge, visible }) => {
       ...data,
       badge_award: null,
     });
+    clearTimeout(timeout);
+    bg1?.destroy();
+    bg2?.destroy();
   };
   const handleCancel = async () => {
     await handle();
@@ -58,8 +66,38 @@ const BadgeModal: FC<BadgeModalProps> = ({ badge, visible }) => {
     navigate(url);
   };
 
+  useEffect(() => {
+    const DURATION = 8000;
+    const LENGTH = 200;
+    const bgNode = document.documentElement || document.body;
+
+    if (visible) {
+      const paranetNode = document.getElementById('badgeModal')?.parentNode;
+
+      bg1 = new AnimateGift({
+        elm: paranetNode,
+        width: bgNode.clientWidth,
+        height: bgNode.clientHeight,
+        length: LENGTH,
+        duration: DURATION,
+        isLoop: true,
+      });
+
+      timeout = setTimeout(() => {
+        bg2 = new AnimateGift({
+          elm: paranetNode,
+          width: window.innerWidth,
+          height: window.innerHeight,
+          length: LENGTH,
+          duration: DURATION,
+        });
+      }, DURATION / 2);
+    }
+  }, [visible]);
+
   return (
     <Modal
+      id="badgeModal"
       title={t('title')}
       visible={visible}
       onCancel={handleCancel}
diff --git a/ui/src/components/Modal/Modal.tsx b/ui/src/components/Modal/Modal.tsx
index 4d66137a..cbf98c24 100644
--- a/ui/src/components/Modal/Modal.tsx
+++ b/ui/src/components/Modal/Modal.tsx
@@ -22,6 +22,7 @@ import { Button, Modal } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
 export interface Props {
+  id?: string;
   /** header title */
   title?: string;
   children?: React.ReactNode;
@@ -43,6 +44,7 @@ export interface Props {
   className?: string;
 }
 const Index: FC<Props> = ({
+  id = '',
   title = 'title',
   visible = false,
   centered = true,
@@ -63,6 +65,7 @@ const Index: FC<Props> = ({
   const { t } = useTranslation();
   return (
     <Modal
+      id={id}
       className={className}
       scrollable={scrollable}
       show={visible}
diff --git a/ui/src/utils/animateGift.ts b/ui/src/utils/animateGift.ts
new file mode 100644
index 00000000..c53b2f76
--- /dev/null
+++ b/ui/src/utils/animateGift.ts
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Progress from './progress';
+
+export default class Confetti {
+  private parent: HTMLElement;
+
+  private canvas: HTMLCanvasElement;
+
+  private ctx;
+
+  private width: number;
+
+  private height: number;
+
+  private length: number;
+
+  private yRange: number;
+
+  private progress;
+
+  private rotationRange;
+
+  private speedRange;
+
+  private sprites;
+
+  constructor(param) {
+    this.parent = param.elm || document.body;
+    this.canvas = document.createElement('canvas');
+    this.ctx = this.canvas.getContext('2d');
+    this.width = param.width || this.parent.offsetWidth;
+    this.height = param.height || this.parent.offsetHeight;
+    this.length = param.length || Confetti.CONST.PAPER_LENGTH;
+    this.yRange = param.yRange || this.height * 2;
+    this.progress = new Progress({
+      duration: param.duration,
+      isLoop: true,
+      timestamp: null,
+      delta: 0,
+    });
+    this.rotationRange =
+      typeof param.rotationLength === 'number' ? param.rotationRange : 10;
+    this.speedRange =
+      typeof param.speedRange === 'number' ? param.speedRange : 10;
+    this.sprites = [];
+
+    this.canvas.style.cssText = [
+      'display: block',
+      'position: absolute',
+      'top: 0',
+      'left: 0',
+      'pointer-events: none',
+    ].join(';');
+
+    this.render = this.render.bind(this);
+
+    this.build();
+
+    this.parent.appendChild(this.canvas);
+    this.progress.start(performance.now());
+
+    requestAnimationFrame(this.render);
+  }
+
+  static get CONST() {
+    return {
+      SPRITE_WIDTH: 9,
+      SPRITE_HEIGHT: 16,
+      PAPER_LENGTH: 100,
+      DURATION: 8000,
+      ROTATION_RATE: 50,
+      COLORS: [
+        '#EF5350',
+        '#EC407A',
+        '#AB47BC',
+        '#7E57C2',
+        '#5C6BC0',
+        '#42A5F5',
+        '#29B6F6',
+        '#26C6DA',
+        '#26A69A',
+        '#66BB6A',
+        '#9CCC65',
+        '#D4E157',
+        '#FFEE58',
+        '#FFCA28',
+        '#FFA726',
+        '#FF7043',
+        '#8D6E63',
+        '#BDBDBD',
+        '#78909C',
+      ],
+    };
+  }
+
+  build() {
+    for (let i = 0; i < this.length; i += 1) {
+      const canvas = document.createElement('canvas') as HTMLCanvasElement & {
+        position: { initX: number; initY: number };
+        rotation;
+        speed;
+      };
+      const ctx = canvas.getContext('2d');
+
+      canvas.width = Confetti.CONST.SPRITE_WIDTH;
+      canvas.height = Confetti.CONST.SPRITE_HEIGHT;
+
+      canvas.position = {
+        initX: Math.random() * this.width,
+        initY: -canvas.height - Math.random() * this.yRange,
+      };
+
+      canvas.rotation =
+        this.rotationRange / 2 - Math.random() * this.rotationRange;
+      canvas.speed =
+        this.speedRange / 2 + Math.random() * (this.speedRange / 2);
+
+      if (ctx) {
+        ctx.save();
+        ctx.fillStyle =
+          Confetti.CONST.COLORS[
+            Math.floor(Math.random() * Confetti.CONST.COLORS.length)
+          ];
+        ctx.fillRect(0, 0, canvas.width, canvas.height);
+        ctx.restore();
+      }
+
+      this.sprites.push(canvas);
+    }
+  }
+
+  render(now) {
+    const progress = this.progress.tick(now);
+
+    this.canvas.width = this.width;
+    this.canvas.height = this.height;
+
+    for (let i = 0; i < this.length; i += 1) {
+      this.ctx.save();
+      this.ctx.translate(
+        this.sprites[i].position.initX +
+          this.sprites[i].rotation * Confetti.CONST.ROTATION_RATE * progress,
+        this.sprites[i].position.initY + progress * (this.height + this.yRange),
+      );
+      this.ctx.rotate(this.sprites[i].rotation);
+      this.ctx.drawImage(
+        this.sprites[i],
+        (-Confetti.CONST.SPRITE_WIDTH *
+          Math.abs(Math.sin(progress * Math.PI * 2 * this.sprites[i].speed))) /
+          2,
+        -Confetti.CONST.SPRITE_HEIGHT / 2,
+        Confetti.CONST.SPRITE_WIDTH *
+          Math.abs(Math.sin(progress * Math.PI * 2 * this.sprites[i].speed)),
+        Confetti.CONST.SPRITE_HEIGHT,
+      );
+      this.ctx.restore();
+    }
+
+    requestAnimationFrame(this.render);
+  }
+
+  destroy() {
+    this.parent.removeChild(this.canvas);
+  }
+}
diff --git a/ui/src/utils/progress.ts b/ui/src/utils/progress.ts
new file mode 100644
index 00000000..70f4fc54
--- /dev/null
+++ b/ui/src/utils/progress.ts
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class Progress {
+  private timestamp;
+
+  private duration: number;
+
+  private progress: number;
+
+  private delta: number;
+
+  private isLoop: boolean;
+
+  constructor(
+    param = {
+      timestamp: null,
+      duration: 1000,
+      delta: 0,
+      isLoop: true,
+    },
+  ) {
+    this.timestamp = null;
+    this.duration = param.duration || Progress.CONST.DURATION;
+    this.progress = 0;
+    this.delta = 0;
+    this.isLoop = !!param.isLoop;
+
+    this.reset();
+  }
+
+  static get CONST() {
+    return {
+      DURATION: 1000,
+    };
+  }
+
+  reset() {
+    this.timestamp = null;
+  }
+
+  start(now) {
+    this.timestamp = now;
+  }
+
+  tick(now) {
+    if (this.timestamp) {
+      this.delta = now - this.timestamp;
+      this.progress = Math.min(this.delta / this.duration, 1);
+
+      if (this.progress >= 1 && this.isLoop) {
+        this.start(now);
+      }
+
+      return this.progress;
+    }
+    return 0;
+  }
+}
+
+export default Progress;