Browse Source

coupon

master
jiannibang 6 years ago
parent
commit
f0e453ad20
  1. 2
      package.json
  2. 1
      public/model-stride16.json
  3. 107
      src/App.js
  4. 61
      src/App.scss
  5. BIN
      src/Chalet Comprime Cologne Sixty.ttf
  6. 9
      src/components/header/header.js
  7. 7
      src/components/header/header.scss
  8. BIN
      src/components/header/ic_logo.png
  9. BIN
      src/components/meta.png
  10. BIN
      src/components/qrcode.png
  11. 81
      src/components/shop.js
  12. 180
      src/components/shop.scss
  13. 51
      src/data/Shops.js
  14. 22
      src/data/format.js
  15. 270671
      src/data/mapData.json
  16. 7790
      src/data/shopInfo.json
  17. BIN
      src/images/coupon/1.png
  18. BIN
      src/images/coupon/2.png
  19. BIN
      src/images/coupon/3.png
  20. BIN
      src/images/coupon/4.png
  21. BIN
      src/images/coupon/a.png
  22. BIN
      src/images/coupon/b.png
  23. BIN
      src/images/coupon/c.png
  24. BIN
      src/images/coupon/c1.png
  25. BIN
      src/images/coupon/c2.png
  26. BIN
      src/images/coupon/c3.png
  27. BIN
      src/images/coupon/c4.png
  28. BIN
      src/images/coupon/d.png
  29. 11
      src/images/coupon/image-helper.js
  30. 10
      src/index.css
  31. 1
      src/model-stride16.json
  32. 20
      src/models/Sphere.js
  33. 22
      src/models/Wall.js
  34. 10
      yarn.lock

2
package.json

@ -13,11 +13,13 @@
"bezier-easing": "^2.1.0",
"canvas-sketch-util": "^1.10.0",
"matter-js": "^0.14.2",
"moment": "^2.27.0",
"node-sass": "^4.13.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"react-swipeable": "^5.5.1",
"stats.js": "^0.17.0",
"three": "^0.117.1",
"touches": "^1.2.2"
},

1
public/model-stride16.json

File diff suppressed because one or more lines are too long

107
src/App.js

@ -4,18 +4,25 @@ import Wall, { easeIn } from "./models/Wall";
import Sphere from "./models/Sphere";
import createTouches from "touches";
import "./App.scss";
// import shops from "./data/format";
import shops from "./data/Shops";
import Shop from "./components/shop";
import touchIcon from "./touch.png";
import model from "./model-stride16.json";
import moment from "moment";
import "moment/locale/zh-cn";
moment.locale("zh-cn");
const videoWidth = 600;
const videoHeight = 500;
const width = window.innerWidth;
const height = window.innerHeight;
const radius = 450;
const displacement = 400;
const POSENET_URL =
"https://lg-cjdqwkbo-1256266248.cos.ap-shanghai.myqcloud.com/mobile-net/50/model-stride16.json";
const radius = 500;
const displacement = 550;
let item = null;
window.oncontextmenu = function (e) {
e.preventDefault();
};
const App = () => {
const [posenetModel, setPosenetModel] = useState(undefined);
const [video, setVideo] = useState(undefined);
@ -24,27 +31,10 @@ const App = () => {
const [sphere, setSphere] = useState(undefined);
const [list, setList] = useState([]);
const [minutes, setMinutes] = useState(0);
const [seconds, setSeconds] = useState(0);
const shop = shops[shopIndex];
const [itemData, setItem] = useState(null);
async function poseDetectionFrame() {
if (posenetModel) {
const poses = await posenetModel.estimatePoses(video, {
flipHorizontal: true,
decodingMethod: "single-person",
});
const minPoseConfidence = 0.3;
poses.forEach(({ score, keypoints }) => {
if (score >= minPoseConfidence) {
keypoints
.filter(({ part }) => part === "nose")
.forEach(({ position: { x } }) => {
if (sphere) sphere.rePosition(x / videoWidth);
});
}
});
}
requestAnimationFrame(poseDetectionFrame);
}
const [resetTimer, setResetTimer] = useState(null);
useEffect(() => {
if (!posenetModel) {
@ -55,7 +45,7 @@ const App = () => {
outputStride: 16,
inputResolution: 500,
multiplier: 0.5,
modelUrl: POSENET_URL,
model,
})
.then((model) => {
setPosenetModel(model);
@ -70,7 +60,7 @@ const App = () => {
navigator.mediaDevices
.getUserMedia(constraints)
.then(function (mediaStream) {
var video = document.createElement("video");
var video = document.getElementById("video");
video.width = videoWidth;
video.height = videoHeight;
video.srcObject = mediaStream;
@ -82,8 +72,6 @@ const App = () => {
.catch(function (err) {
console.log(err.name + ": " + err.message);
});
} else {
poseDetectionFrame();
}
}, [video]);
@ -103,6 +91,30 @@ const App = () => {
return wall;
};
useEffect(() => {
if (seconds) {
if (posenetModel && video && !item)
posenetModel
.estimatePoses(video, {
flipHorizontal: true,
decodingMethod: "single-person",
})
.then((poses) => {
const minPoseConfidence = 0.3;
poses.forEach(({ score, keypoints }) => {
if (score >= minPoseConfidence) {
keypoints
.filter(({ part }) => part === "nose")
.forEach(({ position: { x } }) => {
if (sphere) sphere.rePosition(x / videoWidth);
});
}
});
});
setSeconds(0);
}
}, [seconds]);
useEffect(() => {
if (wall && sphere) {
wall.attachSphere(sphere);
@ -123,13 +135,30 @@ const App = () => {
}
}, [sphere]);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setMinutes((minutes) => minutes + 1);
}, 1000 * 60 * 60);
}, 1000 * 60);
return () => clearInterval(interval);
}, []);
const unFocus = () => {
item = null;
setItem(item);
sphere.displacement = displacement;
};
const startTiming = () => {
if (resetTimer) clearInterval(resetTimer);
setResetTimer(setInterval(unFocus, 60 * 10 * 1000));
};
// useEffect(() => {
// if (minutes > 0) {
// wall.dispose().then(() => {
@ -158,11 +187,19 @@ const App = () => {
// }, [sphere]);
return (
<div className="App">
{list.map(([key, el]) => (
<div className="App" onClick={startTiming}>
<div className="header">
<div className="left">{moment().format("HH:mm")}</div>
<div className="right">
<div className="r1">{moment().format("YYYY-MM-DD")}</div>
<div className="r2">{moment().format("dddd")}</div>
</div>
</div>
<video id="video"></video>
{list.map(([key, el, i]) => (
<div
className="block-wrapper"
key={key}
key={`${key}_${i}`}
style={{ ...el.style, backgroundImage: "none" }}
>
<div
@ -181,7 +218,9 @@ const App = () => {
sphere.displacement = displacement + easeIn(t) * 200;
}, 1000 / 30);
}
setItem({ dom: e.target, item: el.data, shop });
item = { dom: e.target, item: el.data, shop };
setItem(item);
sphere.rePosition(1 / 2);
}}
></div>
</div>
@ -192,7 +231,7 @@ const App = () => {
style={sphere.style}
>
<div className={"ball" + (!itemData ? " small" : "")}></div>
{itemData && <Shop data={itemData}></Shop>}
{itemData && <Shop data={itemData} unFocus={unFocus}></Shop>}
{!itemData && <img className="touch-icon" src={touchIcon}></img>}
</div>
)}

61
src/App.scss

@ -1,16 +1,62 @@
* {
box-sizing: border-box;
user-select: none;
}
.App {
position: relative;
width: 100vw;
height: 100vh;
text-align: center;
background: #f6f8f9;
background: linear-gradient(180deg, #a5a1a1, #eceaea 99%);
// background-image: url(./images/bg.jpg);
display: flex;
flex-direction: column;
overflow: hidden;
.header {
position: absolute;
top: 44px;
left: 44px;
display: flex;
.left {
height: 44px;
font-size: 48px;
font-family: ChaletComprime, ChaletComprime-CologneSixty;
font-weight: CologneSixty;
text-align: left;
color: rgba(0, 0, 0, 0.85);
line-height: 44px;
margin-right: 8px;
}
.right {
display: flex;
flex-direction: column;
.r1 {
height: 22px;
font-size: 24px;
font-family: ChaletComprime, ChaletComprime-MilanSixty;
font-weight: MilanSixty;
text-align: left;
color: rgba(0, 0, 0, 0.5);
line-height: 22px;
}
.r2 {
height: 22px;
font-size: 12px;
font-family: FZLTHK;
text-align: left;
color: rgba(0, 0, 0, 0.5);
line-height: 22px;
}
}
}
video {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: none;
}
canvas {
position: absolute;
top: 0;
@ -23,9 +69,18 @@
position: absolute;
// border: 1px solid rgb(224, 224, 224);
opacity: 1;
padding: 8px 10px;
box-sizing: border-box;
&::before {
content: "";
position: absolute;
top: -5px;
left: -5px;
bottom: -5px;
right: -5px;
background: #fff;
z-index: -1;
border-radius: 10px;
}
box-sizing: border-box;
.block {
width: 100%;
height: 100%;

BIN
src/Chalet Comprime Cologne Sixty.ttf

Binary file not shown.

9
src/components/header/header.js

@ -0,0 +1,9 @@
import React from "react";
import "./header.scss";
const Header = () => (
<div className="header">
<div className="left"></div>
<div className="right"></div>
</div>
);
export default Header;

7
src/components/header/header.scss

@ -0,0 +1,7 @@
.header {
position: absolute;
top: 0;
left: 0;
width: 100vw;
display: flex;
}

BIN
src/components/header/ic_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/components/meta.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/components/qrcode.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

81
src/components/shop.js

@ -1,8 +1,10 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, Component } from "react";
import { Swipeable } from "react-swipeable";
import "./shop.scss";
const itemVh = 530;
const Shop = ({ data }) => {
import meta from "./meta.png";
import qrcode from "./qrcode.png";
const itemVh = 694;
const Shop = ({ data, unFocus }) => {
const { item, shop, dom } = data;
const { url } = item;
const { name, items, imgWidth, imgHeight, categoryLogoMap } = shop;
@ -17,14 +19,14 @@ const Shop = ({ data }) => {
useEffect(() => {
const { dom, item } = data;
const { width, height, left, top } = dom.getBoundingClientRect();
console.log(width, height);
setPhStyle({
position: "absolute",
width: width + "px",
height: height + "px",
left: `calc(${left}px - ((100vw - ${itemVw}px) / 2))`,
top: `calc(${top}px - ((100vh - ${itemVh}px) / 2))`,
height: (width / 2) * 3 + "px",
left: `${left - 40}px`,
top: `${top - 460}px`,
opacity: dom.style.opacity,
padding: "8px 10px",
border: `1px solid rgb(224, 224, 224)`,
});
setTimeout(() => {
@ -32,15 +34,11 @@ const Shop = ({ data }) => {
setTimeout(() => {
setPhStyle({
position: "absolute",
width: itemVw + "px",
height: itemVh + "px",
left: 0,
right: 0,
top: "77px",
margin: "auto",
width: 300 + "px",
height: 450 + "px",
left: "165px",
top: "215px",
opacity: 1,
padding: "45px 35px",
boxShadow: `0px 0px 39px 0px rgba(162,162,162,0.5)`,
});
setTimeout(() => {
setStage(1);
@ -63,41 +61,60 @@ const Shop = ({ data }) => {
)}
{stage >= 1 && (
<div className="shop">
<div className="middle">
<div className="middle row">
<div className="arrow left" onClick={pre}>
{"<"}
</div>
<div className="fill"></div>
{[list[index], list[index], list[index]].map((item, i) => (
<Swipeable
className="img-wrapper"
className={
"img-wrapper row" +
(i === 0 ? " left" : i === 1 ? "" : " right")
}
key={"a" + i}
onSwipedLeft={nxt}
onSwipedRight={pre}
>
{list[index] && (
{item && (
<>
<div
className={"img"}
style={{
backgroundImage: `url(${list[index].url})`,
backgroundImage: `url(${item.url})`,
}}
></div>
<div className="col fill">
<div className="price">{item.price}</div>
<div className="fill row bottom">
<div className="col">
<img src={categoryUrl}></img>
<div className="fill"></div>
<div className="title">使用说明</div>
<div className="desc">{item.desc}</div>
</div>
<div className="fill"></div>
<div className="col">
<div
className="meta"
style={{ backgroundImage: `url(${meta})` }}
></div>
<div className="fill"></div>
<img src={qrcode}></img>
</div>
</div>
</div>
</>
)}
</Swipeable>
))}
<div className="fill"></div>
<div className="arrow right" onClick={nxt}>
{">"}
</div>
</div>
<div className="down">
<div className="avatar-wrapper">
<div
className="avatar"
style={{ backgroundImage: `url(${categoryUrl})` }}
></div>
</div>
<div className="texts">
<div className="name">{list[index] && list[index].name}</div>
<div className="price">{list[index] && list[index].price}</div>
<div className="desc">{list[index] && list[index].desc}</div>
</div>
<div className="down" onClick={unFocus}>
</div>
</div>
)}

180
src/components/shop.scss

@ -3,7 +3,18 @@
width: 100%;
height: 100%;
box-sizing: border-box;
padding-top: 77px;
padding: 50px;
padding-top: 203px;
.row {
display: flex;
}
.col {
display: flex;
flex-direction: column;
}
.fill {
flex: 1;
}
@keyframes fadeIn {
from {
opacity: 0;
@ -13,50 +24,18 @@
}
}
.down {
position: relative;
animation: fadeIn 0.5s ease-in-out;
margin: 40px 100px 0 100px;
padding-left: 200px;
text-align: left;
.avatar-wrapper {
position: absolute;
top: 0;
left: 0;
width: 180px;
height: 180px;
border: 1px solid #cacaca;
padding: 18px;
background: #fdfdfd;
.avatar {
width: 100%;
height: 100%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
}
.texts {
margin: auto;
.name {
color: #333;
font-size: 30px;
line-height: 40px;
margin: auto;
}
.price {
color: #333;
font-size: 20px;
line-height: 20px;
}
.desc {
color: #676767;
margin-top: 25px;
font-size: 18px;
line-height: 22px;
max-height: 95px;
overflow: scroll;
}
}
margin-top: 30px;
width: 90px;
height: 90px;
background: #ffffff;
border-radius: 50%;
box-shadow: 0px 0px 23px 0px rgba(0, 0, 0, 0.1);
vertical-align: baseline;
text-align: center;
font-size: 32px;
line-height: 90px;
color: #ff7841;
}
.middle {
position: relative;
@ -67,14 +46,15 @@
top: 0;
bottom: 0;
margin: auto;
width: 40px;
line-height: 60px;
height: 60px;
font-size: 60px;
width: 57px;
line-height: 100px;
height: 100px;
font-size: 100px;
font-weight: bold;
color: #dadbdd;
color: #fff;
transform-origin: center;
transform: scaleY(2);
text-shadow: 0px 0px 23px rgba(0, 0, 0, 0.1);
&.left {
left: 0;
}
@ -83,18 +63,105 @@
}
}
.img-wrapper {
width: 700px;
height: 530px;
background: #fefefe;
padding: 35px 45px;
box-shadow: 0px 0px 39px 0px rgba(162, 162, 162, 0.5);
width: 694px;
height: 474px;
background: #fff;
padding: 12px;
border-radius: 23px;
box-shadow: 0px 0px 23px 0px rgba(0, 0, 0, 0.1);
&.left {
position: absolute;
left: 19px;
transform: scale(0.8);
transform-origin: left;
z-index: -1;
opacity: 0.5;
}
&.right {
position: absolute;
right: 19px;
transform: scale(0.8);
transform-origin: right;
z-index: -1;
opacity: 0.5;
}
.img {
width: 100%;
height: 100%;
width: 300px;
height: 450px;
background-size: cover;
background-position: center;
margin: auto;
}
.price {
text-align: center;
flex: 0 0 254px;
font-size: 105px;
height: 254px;
font-family: ChaletComprime, ChaletComprime-CologneSixty;
font-weight: CologneSixty;
color: rgba(0, 0, 0, 0.8);
line-height: 254px;
&::after {
content: "";
font-size: 36px;
font-family: SourceHanSansCN, SourceHanSansCN-Bold;
font-weight: 700;
color: rgba(0, 0, 0, 0.5);
}
}
.bottom {
padding: 0 4px 0 24px;
.title {
font-size: 18px;
font-family: SourceHanSansCN, SourceHanSansCN-Bold;
font-weight: 700;
text-align: left;
color: rgba(0, 0, 0, 0.5);
line-height: 18px;
}
.desc {
width: 198px;
max-height: 108px;
font-size: 14px;
font-family: SourceHanSansCN, SourceHanSansCN-Normal;
font-weight: Normal;
text-align: justify;
color: rgba(0, 0, 0, 0.5);
line-height: 23px;
padding-right: 8px;
overflow-y: scroll;
padding-top: 10px;
&::-webkit-scrollbar {
width: 3px;
}
&::-webkit-scrollbar-track {
height: 101px;
background: rgba(0, 0, 0, 0.06);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
width: 3px;
height: 50px;
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
}
}
.meta {
width: 120px;
height: 47px;
background-size: cover;
position: relative;
text-align: center;
&::after {
content: "扫码领券";
font-size: 18px;
font-family: SourceHanSansCN, SourceHanSansCN-Bold;
font-weight: 700;
color: #ffffff;
line-height: 40px;
}
}
}
}
}
}
@ -103,7 +170,6 @@
transition: all 1s ease-out;
background-position: center;
background-size: cover;
background: #fff;
.img {
width: 100%;
height: 100%;

51
src/data/Shops.js

@ -1,7 +1,58 @@
import swatch from "../images/swatch/image-helper";
import uniqlo from "../images/uniqlo/image-helper";
import lego from "../images/lego/image-helper";
import coupon from "../images/coupon/image-helper";
const shops = [
{
name: "优惠券",
categories: ["零食", "生鲜", "饮料", "母婴"],
categoryLogoMap: {
零食: coupon["c1"],
饮料: coupon["c2"],
母婴: coupon["c3"],
生鲜: coupon["c4"],
},
items: [
{
id: 1,
urlSmall: coupon["1"],
url: coupon["a"],
name: "",
category: "零食",
desc: `乐融商城内通用,无使用限额、品类、地域限制,订单中所购商品总额需达到用券限额才能使用。单张订单限用1张全场满减券,按面值金额减免支付。单张订单限用1张全场满减券,按面值金额减免支付。仅限与免邮券同时使用。仅限与免邮券同时使用。 `,
price: 150,
},
{
id: 2,
urlSmall: coupon["2"],
url: coupon["b"],
name: "",
category: "饮料",
desc: `乐融商城内通用,无使用限额、品类、地域限制,订单中所购商品总额需达到用券限额才能使用。单张订单限用1张全场满减券,按面值金额减免支付。单张订单限用1张全场满减券,按面值金额减免支付。仅限与免邮券同时使用。仅限与免邮券同时使用。 `,
price: 80,
},
{
id: 3,
urlSmall: coupon["3"],
url: coupon["c"],
name: "",
category: "母婴",
desc: `乐融商城内通用,无使用限额、品类、地域限制,订单中所购商品总额需达到用券限额才能使用。单张订单限用1张全场满减券,按面值金额减免支付。单张订单限用1张全场满减券,按面值金额减免支付。仅限与免邮券同时使用。仅限与免邮券同时使用。 `,
price: 99,
},
{
id: 4,
urlSmall: coupon["4"],
url: coupon["d"],
name: "",
category: "生鲜",
desc: `乐融商城内通用,无使用限额、品类、地域限制,订单中所购商品总额需达到用券限额才能使用。单张订单限用1张全场满减券,按面值金额减免支付。单张订单限用1张全场满减券,按面值金额减免支付。仅限与免邮券同时使用。仅限与免邮券同时使用。 `,
price: 50,
},
],
imgWidth: 90,
imgHeight: 180,
},
{
name: "LEGO",
categories: ["哈利·波特", "机械组", "星球大战"],

22
src/data/format.js

@ -0,0 +1,22 @@
import shopInfo from "./shopInfo.json";
const shopsGroupedByFormat = shopInfo.reduce((acc, { shopList }) => {
shopList.forEach((shop) => {
Object.assign(shop, {
url: "http://1000my.com/MallSite/" + encodeURIComponent(shop.logoPath),
});
acc[shop.shopFormat] = acc[shop.shopFormat]
? [...acc[shop.shopFormat], shop]
: [shop];
});
return acc;
}, {});
const formats = Object.entries(shopsGroupedByFormat)
// .filter(([_, items]) => items.length > 13)
.map(([name, items]) => ({
name,
items,
imgWidth: 160,
imgHeight: 120,
}));
console.log(formats);
export default formats;

270671
src/data/mapData.json

File diff suppressed because it is too large

7790
src/data/shopInfo.json

File diff suppressed because it is too large

BIN
src/images/coupon/1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

BIN
src/images/coupon/2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

BIN
src/images/coupon/3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
src/images/coupon/4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
src/images/coupon/a.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

BIN
src/images/coupon/b.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

BIN
src/images/coupon/c.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
src/images/coupon/c1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/images/coupon/c2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/images/coupon/c3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/images/coupon/c4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
src/images/coupon/d.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

11
src/images/coupon/image-helper.js

@ -0,0 +1,11 @@
function importAll(r) {
let obj = {};
r.keys().forEach((key) => {
obj[key.replace("./", "").replace(".jpg", "").replace(".png", "")] = r(key);
});
return obj;
}
const uniqlo = importAll(require.context("./", false, /\.(png|jpe?g|svg)$/));
export default uniqlo;

10
src/index.css

@ -1,13 +1,17 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
@font-face {
font-family: "ChaletComprime";
src: url("./Chalet\ Comprime\ Cologne\ Sixty.ttf") format("truetype");
}

1
src/model-stride16.json

File diff suppressed because one or more lines are too long

20
src/models/Sphere.js

@ -1,11 +1,29 @@
import { Vector3 } from "three";
const T = 1000;
export default class Sphere {
interval = null;
constructor({ radius, displacement, x, y, update, width }) {
Object.assign(this, { radius, displacement, x, y, update, width });
}
rePosition(ratio) {
const lastX = this.x;
const x = ratio * this.width;
Object.assign(this, { x });
const offset = x - this.x;
let t = 0;
if (this.interval !== null) {
clearInterval(this.interval);
this.interval = null;
}
this.interval = setInterval(() => {
t += T / 60;
if (t > T) {
clearInterval(this.interval);
this.interval = null;
this.x = x;
} else {
this.x = lastX + (offset * t) / T;
}
}, T / 60);
}
get point() {
return new Vector3(this.x, this.y, 0);

22
src/models/Wall.js

@ -1,10 +1,17 @@
import { Vector3 } from "three";
import BezierEasing from "bezier-easing";
import Stats from "stats.js";
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
export const easeIn = BezierEasing(0.5, 0.16, 0.74, 0.38);
export const easeOut = BezierEasing(0, 0, 0.58, 1);
const enterDuration = 3000;
const exitDuration = 5000;
const margin = 30;
const margin = 55;
class El {
constructor(seed) {
const { data, row, wall, key, index } = seed;
@ -28,13 +35,13 @@ class El {
}
const top = point.y - imgHeight / 2;
const left = point.x - imgWidth / 2;
const { url } = data;
const { urlSmall } = data;
const style = {
width: `${imgWidth}px`,
height: `${imgHeight}px`,
top: `${top}px`,
left: `${left}px`,
backgroundImage: `url(${url})`,
backgroundImage: `url(${urlSmall})`,
transform: `scale(${scale})`,
opcaity: scale,
zIndex: wall.rowNum - Math.floor(Math.abs(wall.rowNum / 2 - row.index)),
@ -151,7 +158,7 @@ export default class Wall {
}) {
const blockWidth = imgWidth + margin;
const blockHeight = imgHeight + margin;
const colNum = Math.floor(containerWidth / blockWidth);
const colNum = Math.floor(containerWidth / blockWidth) + 1;
const rowNum = Math.floor(containerHeight / blockHeight);
const elNum = items.length;
const ratioSpeed = speed / (containerWidth / colNum);
@ -176,7 +183,10 @@ export default class Wall {
}
getNext() {
const { items } = this;
return items[Math.floor(Math.random() * items.length)];
const next = items[this.index];
this.index++;
if (this.index === items.length) this.index = 0;
return next;
}
async init() {
const { rowNum } = this;
@ -197,6 +207,7 @@ export default class Wall {
return this;
}
animate = () => {
stats.begin();
if (!this.isRunning) return;
const now = performance.now();
const dt = Math.min(this.maxDeltaTime, (now - this.#lastTime) / 1000);
@ -211,6 +222,7 @@ export default class Wall {
this.time += dt;
this.#lastTime = now;
this.#rafID = window.requestAnimationFrame(this.animate);
stats.end();
};
async dispose() {
if (this.#rafID === null) return;

10
yarn.lock

@ -6270,6 +6270,11 @@ mixin-object@^2.0.1:
dependencies:
minimist "^1.2.5"
moment@^2.27.0:
version "2.27.0"
resolved "https://registry.npm.taobao.org/moment/download/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha1-i/9OPiaiNiIN/j423nVrbrqgEF0=
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -8861,6 +8866,11 @@ static-extend@^0.1.1:
define-property "^0.2.5"
object-copy "^0.1.0"
stats.js@^0.17.0:
version "0.17.0"
resolved "https://registry.npm.taobao.org/stats.js/download/stats.js-0.17.0.tgz#b1c3dc46d94498b578b7fd3985b81ace7131cc7d"
integrity sha1-scPcRtlEmLV4t/05hbgaznExzH0=
"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.npm.taobao.org/statuses/download/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"

Loading…
Cancel
Save