11 changed files with 1278 additions and 82 deletions
@ -1,38 +0,0 @@ |
|||||
.App { |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.App-logo { |
|
||||
height: 40vmin; |
|
||||
pointer-events: none; |
|
||||
} |
|
||||
|
|
||||
@media (prefers-reduced-motion: no-preference) { |
|
||||
.App-logo { |
|
||||
animation: App-logo-spin infinite 20s linear; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.App-header { |
|
||||
background-color: #282c34; |
|
||||
min-height: 100vh; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
font-size: calc(10px + 2vmin); |
|
||||
color: white; |
|
||||
} |
|
||||
|
|
||||
.App-link { |
|
||||
color: #61dafb; |
|
||||
} |
|
||||
|
|
||||
@keyframes App-logo-spin { |
|
||||
from { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
to { |
|
||||
transform: rotate(360deg); |
|
||||
} |
|
||||
} |
|
||||
@ -1,26 +1,157 @@ |
|||||
import React from 'react'; |
|
||||
import logo from './logo.svg'; |
|
||||
import './App.css'; |
|
||||
|
import React, { useState, useEffect } from "react"; |
||||
|
|
||||
|
import * as posenet from "@tensorflow-models/posenet"; |
||||
|
import Wall from "./wall/Wall"; |
||||
|
import Sphere from "./wall/Sphere"; |
||||
|
import createTouches from "touches"; |
||||
|
import "./App.scss"; |
||||
|
import i1 from "./images/1.jpg"; |
||||
|
import i2 from "./images/2.jpg"; |
||||
|
import i3 from "./images/3.jpg"; |
||||
|
|
||||
|
const videoWidth = 600; |
||||
|
const videoHeight = 500; |
||||
|
const width = 1080; |
||||
|
const height = 1920; |
||||
|
const radius = 412; |
||||
|
const displacement = 412; |
||||
|
const POSENET_URL = |
||||
|
"https://lg-cjdqwkbo-1256266248.cos.ap-shanghai.myqcloud.com/mobile-net/50/model-stride16.json"; |
||||
|
|
||||
|
const Block = ({ style }) => <div style={style}></div>; |
||||
|
|
||||
|
const Mist = ({ style }) => ( |
||||
|
<div className="mist" style={style}> |
||||
|
<div className="ball"></div> |
||||
|
<div className="text"> |
||||
|
touch <br /> |
||||
|
item |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
|
||||
|
const App = () => { |
||||
|
const [posenetModel, setPosenetModel] = useState(undefined); |
||||
|
const [video, setVideo] = useState(undefined); |
||||
|
const [wall, setWall] = useState(undefined); |
||||
|
const [sphere, setSphere] = useState(undefined); |
||||
|
const [list, setList] = useState([]); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!posenetModel) { |
||||
|
console.log("loading posenet model..."); |
||||
|
posenet |
||||
|
.load({ |
||||
|
architecture: "MobileNetV1", |
||||
|
outputStride: 16, |
||||
|
inputResolution: 500, |
||||
|
multiplier: 0.5, |
||||
|
modelUrl: POSENET_URL, |
||||
|
}) |
||||
|
.then((model) => { |
||||
|
setPosenetModel(model); |
||||
|
console.log("model loaded."); |
||||
|
}); |
||||
|
} |
||||
|
}, [posenetModel]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!video) { |
||||
|
var constraints = { audio: false, video: { width: 1080, height: 1920 } }; |
||||
|
navigator.mediaDevices |
||||
|
.getUserMedia(constraints) |
||||
|
.then(function (mediaStream) { |
||||
|
var video = document.createElement("video"); |
||||
|
video.width = videoWidth; |
||||
|
video.height = videoHeight; |
||||
|
video.srcObject = mediaStream; |
||||
|
video.onloadedmetadata = function (e) { |
||||
|
video.play(); |
||||
|
setVideo(video); |
||||
|
}; |
||||
|
}) |
||||
|
.catch(function (err) { |
||||
|
console.log(err.name + ": " + err.message); |
||||
|
}); |
||||
|
} else { |
||||
|
poseDetectionFrame(); |
||||
|
} |
||||
|
}, [video]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!wall) { |
||||
|
const wall = new Wall({ |
||||
|
items: [{ url: i1 }, { url: i2 }, { url: i3 }], |
||||
|
imgWidth: 90, |
||||
|
imgHeight: 100, |
||||
|
containerWidth: 1080, |
||||
|
containerHeight: 1920, |
||||
|
speed: 1, |
||||
|
onListChange: setList, |
||||
|
}); |
||||
|
wall.init(); |
||||
|
setWall(wall); |
||||
|
wall.getList(); |
||||
|
} else { |
||||
|
let sphere = new Sphere({ |
||||
|
x: width / 2, |
||||
|
y: height / 2, |
||||
|
radius, |
||||
|
displacement, |
||||
|
width, |
||||
|
}); |
||||
|
if (wall) { |
||||
|
wall.attachSphere(sphere); |
||||
|
} |
||||
|
setSphere(sphere); |
||||
|
} |
||||
|
return () => { |
||||
|
if (wall) wall.dispose(); |
||||
|
}; |
||||
|
}, [wall]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (sphere) { |
||||
|
const container = document.querySelector(".App"); |
||||
|
const touchHandler = createTouches(container, { |
||||
|
target: container, |
||||
|
filtered: true, |
||||
|
}); |
||||
|
touchHandler.on("move", (_, [x, y]) => { |
||||
|
sphere.x = x; |
||||
|
sphere.y = y; |
||||
|
}); |
||||
|
} |
||||
|
}, [sphere]); |
||||
|
|
||||
function App() { |
|
||||
return ( |
return ( |
||||
<div className="App"> |
<div className="App"> |
||||
<header className="App-header"> |
|
||||
<img src={logo} className="App-logo" alt="logo" /> |
|
||||
<p> |
|
||||
Edit <code>src/App.js</code> and save to reload. |
|
||||
</p> |
|
||||
<a |
|
||||
className="App-link" |
|
||||
href="https://reactjs.org" |
|
||||
target="_blank" |
|
||||
rel="noopener noreferrer" |
|
||||
> |
|
||||
Learn React |
|
||||
</a> |
|
||||
</header> |
|
||||
|
{list.map(([key, el]) => ( |
||||
|
<Block key={key} style={el.style}></Block> |
||||
|
))} |
||||
|
{sphere && <Mist style={sphere.style}></Mist>} |
||||
</div> |
</div> |
||||
); |
); |
||||
} |
|
||||
|
}; |
||||
|
|
||||
export default App; |
export default App; |
||||
|
|||||
@ -0,0 +1,40 @@ |
|||||
|
.App { |
||||
|
position: relative; |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
text-align: center; |
||||
|
background: rgb(218, 232, 255); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
overflow: hidden; |
||||
|
canvas { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.mist { |
||||
|
position: absolute; |
||||
|
z-index: 100; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
.ball { |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
filter: blur(50px); |
||||
|
border-radius: 50%; |
||||
|
background: rgba(255, 255, 255, 0.8); |
||||
|
} |
||||
|
.text { |
||||
|
text-align: center; |
||||
|
color: #000; |
||||
|
font-size: 60px; |
||||
|
margin: auto; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 43 KiB |
@ -0,0 +1,205 @@ |
|||||
|
import * as posenet from "@tensorflow-models/posenet"; |
||||
|
import * as tf from "@tensorflow/tfjs"; |
||||
|
|
||||
|
const color = "aqua"; |
||||
|
const boundingBoxColor = "red"; |
||||
|
const lineWidth = 2; |
||||
|
|
||||
|
export const tryResNetButtonName = "tryResNetButton"; |
||||
|
export const tryResNetButtonText = "[New] Try ResNet50"; |
||||
|
const tryResNetButtonTextCss = "width:100%;text-decoration:underline;"; |
||||
|
const tryResNetButtonBackgroundCss = "background:#e61d5f;"; |
||||
|
|
||||
|
function isAndroid() { |
||||
|
return /Android/i.test(navigator.userAgent); |
||||
|
} |
||||
|
|
||||
|
function isiOS() { |
||||
|
return /iPhone|iPad|iPod/i.test(navigator.userAgent); |
||||
|
} |
||||
|
|
||||
|
export function isMobile() { |
||||
|
return isAndroid() || isiOS(); |
||||
|
} |
||||
|
|
||||
|
function setDatGuiPropertyCss(propertyText, liCssString, spanCssString = "") { |
||||
|
var spans = document.getElementsByClassName("property-name"); |
||||
|
for (var i = 0; i < spans.length; i++) { |
||||
|
var text = spans[i].textContent || spans[i].innerText; |
||||
|
if (text == propertyText) { |
||||
|
spans[i].parentNode.parentNode.style = liCssString; |
||||
|
if (spanCssString !== "") { |
||||
|
spans[i].style = spanCssString; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function updateTryResNetButtonDatGuiCss() { |
||||
|
setDatGuiPropertyCss( |
||||
|
tryResNetButtonText, |
||||
|
tryResNetButtonBackgroundCss, |
||||
|
tryResNetButtonTextCss |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Toggles between the loading UI and the main canvas UI. |
||||
|
*/ |
||||
|
export function toggleLoadingUI( |
||||
|
showLoadingUI, |
||||
|
loadingDivId = "loading", |
||||
|
mainDivId = "main" |
||||
|
) { |
||||
|
if (showLoadingUI) { |
||||
|
document.getElementById(loadingDivId).style.display = "block"; |
||||
|
document.getElementById(mainDivId).style.display = "none"; |
||||
|
} else { |
||||
|
document.getElementById(loadingDivId).style.display = "none"; |
||||
|
document.getElementById(mainDivId).style.display = "block"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toTuple({ y, x }) { |
||||
|
return [y, x]; |
||||
|
} |
||||
|
|
||||
|
export function drawPoint(ctx, y, x, r, color) { |
||||
|
ctx.beginPath(); |
||||
|
ctx.arc(x, y, r, 0, 2 * Math.PI); |
||||
|
ctx.fillStyle = color; |
||||
|
ctx.fill(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Draws a line on a canvas, i.e. a joint |
||||
|
*/ |
||||
|
export function drawSegment([ay, ax], [by, bx], color, scale, ctx) { |
||||
|
ctx.beginPath(); |
||||
|
ctx.moveTo(ax * scale, ay * scale); |
||||
|
ctx.lineTo(bx * scale, by * scale); |
||||
|
ctx.lineWidth = lineWidth; |
||||
|
ctx.strokeStyle = color; |
||||
|
ctx.stroke(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Draws a pose skeleton by looking up all adjacent keypoints/joints |
||||
|
*/ |
||||
|
export function drawSkeleton(keypoints, minConfidence, ctx, scale = 1) { |
||||
|
const adjacentKeyPoints = posenet.getAdjacentKeyPoints( |
||||
|
keypoints, |
||||
|
minConfidence |
||||
|
); |
||||
|
|
||||
|
adjacentKeyPoints.forEach((keypoints) => { |
||||
|
drawSegment( |
||||
|
toTuple(keypoints[0].position), |
||||
|
toTuple(keypoints[1].position), |
||||
|
color, |
||||
|
scale, |
||||
|
ctx |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Draw pose keypoints onto a canvas |
||||
|
*/ |
||||
|
export function drawKeypoints(keypoints, minConfidence, ctx, scale = 1) { |
||||
|
for (let i = 0; i < keypoints.length; i++) { |
||||
|
const keypoint = keypoints[i]; |
||||
|
|
||||
|
if (keypoint.score < minConfidence) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
const { y, x } = keypoint.position; |
||||
|
drawPoint(ctx, y * scale, x * scale, 3, color); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Draw the bounding box of a pose. For example, for a whole person standing |
||||
|
* in an image, the bounding box will begin at the nose and extend to one of |
||||
|
* ankles |
||||
|
*/ |
||||
|
export function drawBoundingBox(keypoints, ctx) { |
||||
|
const boundingBox = posenet.getBoundingBox(keypoints); |
||||
|
|
||||
|
ctx.rect( |
||||
|
boundingBox.minX, |
||||
|
boundingBox.minY, |
||||
|
boundingBox.maxX - boundingBox.minX, |
||||
|
boundingBox.maxY - boundingBox.minY |
||||
|
); |
||||
|
|
||||
|
ctx.strokeStyle = boundingBoxColor; |
||||
|
ctx.stroke(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Converts an arary of pixel data into an ImageData object |
||||
|
*/ |
||||
|
export async function renderToCanvas(a, ctx) { |
||||
|
const [height, width] = a.shape; |
||||
|
const imageData = new ImageData(width, height); |
||||
|
|
||||
|
const data = await a.data(); |
||||
|
|
||||
|
for (let i = 0; i < height * width; ++i) { |
||||
|
const j = i * 4; |
||||
|
const k = i * 3; |
||||
|
|
||||
|
imageData.data[j + 0] = data[k + 0]; |
||||
|
imageData.data[j + 1] = data[k + 1]; |
||||
|
imageData.data[j + 2] = data[k + 2]; |
||||
|
imageData.data[j + 3] = 255; |
||||
|
} |
||||
|
|
||||
|
ctx.putImageData(imageData, 0, 0); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Draw an image on a canvas |
||||
|
*/ |
||||
|
export function renderImageToCanvas(image, size, canvas) { |
||||
|
canvas.width = size[0]; |
||||
|
canvas.height = size[1]; |
||||
|
const ctx = canvas.getContext("2d"); |
||||
|
|
||||
|
ctx.drawImage(image, 0, 0); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Draw heatmap values, one of the model outputs, on to the canvas |
||||
|
* Read our blog post for a description of PoseNet's heatmap outputs |
||||
|
* https://medium.com/tensorflow/real-time-human-pose-estimation-in-the-browser-with-tensorflow-js-7dd0bc881cd5
|
||||
|
*/ |
||||
|
export function drawHeatMapValues(heatMapValues, outputStride, canvas) { |
||||
|
const ctx = canvas.getContext("2d"); |
||||
|
const radius = 5; |
||||
|
const scaledValues = heatMapValues.mul(tf.scalar(outputStride, "int32")); |
||||
|
|
||||
|
drawPoints(ctx, scaledValues, radius, color); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Used by the drawHeatMapValues method to draw heatmap points on to |
||||
|
* the canvas |
||||
|
*/ |
||||
|
function drawPoints(ctx, points, radius, color) { |
||||
|
const data = points.buffer().values; |
||||
|
|
||||
|
for (let i = 0; i < data.length; i += 2) { |
||||
|
const pointY = data[i]; |
||||
|
const pointX = data[i + 1]; |
||||
|
|
||||
|
if (pointX !== 0 && pointY !== 0) { |
||||
|
ctx.beginPath(); |
||||
|
ctx.arc(pointX, pointY, radius, 0, 2 * Math.PI); |
||||
|
ctx.fillStyle = color; |
||||
|
ctx.fill(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { Vector3 } from "three"; |
||||
|
export default class Sphere { |
||||
|
constructor({ radius, displacement, x, y, update, width }) { |
||||
|
Object.assign(this, { radius, displacement, x, y, update, width }); |
||||
|
} |
||||
|
rePosition(ratio) { |
||||
|
const x = ratio * this.width; |
||||
|
Object.assign(this, { x }); |
||||
|
} |
||||
|
get point() { |
||||
|
return new Vector3(this.x, this.y, 0); |
||||
|
} |
||||
|
get style() { |
||||
|
const { radius, x, y } = this; |
||||
|
return { |
||||
|
width: `${radius * 2}px`, |
||||
|
height: `${radius * 2}px`, |
||||
|
top: `${y - radius}px`, |
||||
|
left: `${x - radius}px`, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
import { Vector3 } from "three"; |
||||
|
export const fps = 60; |
||||
|
export const mspf = 1000 / fps; |
||||
|
|
||||
|
class El { |
||||
|
constructor(seed) { |
||||
|
const { data, row, wall, key, index } = seed; |
||||
|
const { imgWidth, blockWidth, imgHeight, sphere } = wall; |
||||
|
const { ratio, y } = row; |
||||
|
let x = index * blockWidth + blockWidth / 2 - ratio * blockWidth; |
||||
|
const point = new Vector3(x, y, 0); |
||||
|
const targetPoint = new Vector3(x, y, 0); |
||||
|
const spherePoint = sphere ? sphere.point : null; |
||||
|
if (sphere && point.distanceTo(spherePoint) < sphere.displacement) { |
||||
|
const direction = point.clone().sub(spherePoint); |
||||
|
const displacementAmount = sphere.displacement - direction.length(); |
||||
|
direction.setLength(displacementAmount); |
||||
|
direction.add(point); |
||||
|
|
||||
|
point.lerp(direction, 1); // ✨ magic number
|
||||
|
} |
||||
|
|
||||
|
// and move them back to their original position
|
||||
|
if (point.distanceTo(targetPoint) > 0.01) { |
||||
|
point.lerp(targetPoint, 0.27); // ✨ magic number
|
||||
|
} |
||||
|
const top = point.y - imgHeight / 2; |
||||
|
const left = point.x - imgWidth / 2; |
||||
|
const { url } = data; |
||||
|
|
||||
|
const style = { |
||||
|
position: "absolute", |
||||
|
width: `${imgWidth}px`, |
||||
|
height: `${imgHeight}px`, |
||||
|
top: `${top}px`, |
||||
|
left: `${left}px`, |
||||
|
backgroundSize: "cover", |
||||
|
backgroundImage: `url(${url})`, |
||||
|
opacity: 1, |
||||
|
zIndex: row.size - Math.floor(Math.abs(row.size / 2 - row.index)), |
||||
|
}; |
||||
|
Object.assign(this, { seed, data, style, key, point }); |
||||
|
} |
||||
|
update() {} |
||||
|
} |
||||
|
|
||||
|
class Row { |
||||
|
constructor({ wall, index }) { |
||||
|
Object.assign(this, { |
||||
|
wall, |
||||
|
q: [], |
||||
|
minSize: wall.colNum + 1, |
||||
|
index, |
||||
|
ratio: Math.random(), |
||||
|
y: index * wall.blockHeight + wall.blockHeight / 2, |
||||
|
}); |
||||
|
} |
||||
|
get size() { |
||||
|
return this.q.length; |
||||
|
} |
||||
|
init() { |
||||
|
while (this.size < this.minSize) { |
||||
|
this.push(); |
||||
|
} |
||||
|
} |
||||
|
push() { |
||||
|
const { wall } = this; |
||||
|
const data = wall.getNext(); |
||||
|
const key = wall.getKey(); |
||||
|
const seed = { data, row: this, wall, key, index: this.q.length }; |
||||
|
const el = new El(seed); |
||||
|
this.wall.elMap.set(key, el); |
||||
|
this.q.push(el); |
||||
|
} |
||||
|
shift() { |
||||
|
const el = this.q.shift(); |
||||
|
this.wall.elMap.delete(el.key); |
||||
|
} |
||||
|
nextFrame(ratioSpeed) { |
||||
|
this.ratio += ratioSpeed; |
||||
|
if (this.ratio > 1) { |
||||
|
this.shift(); |
||||
|
this.push(); |
||||
|
this.q.forEach((el) => { |
||||
|
el.seed.index--; |
||||
|
}); |
||||
|
this.ratio -= 1; |
||||
|
} |
||||
|
this.q.forEach((el) => { |
||||
|
this.wall.elMap.set(el.key, new El(el.seed)); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default class Wall { |
||||
|
#rafID; |
||||
|
#lastTime; |
||||
|
constructor({ |
||||
|
items, |
||||
|
imgWidth, |
||||
|
imgHeight, |
||||
|
containerWidth, |
||||
|
containerHeight, |
||||
|
speed, |
||||
|
onListChange, |
||||
|
}) { |
||||
|
const colNum = Math.floor(containerWidth / imgWidth); |
||||
|
const rowNum = Math.floor(containerHeight / imgHeight); |
||||
|
const elNum = items.length; |
||||
|
const ratioSpeed = speed / (containerWidth / colNum); |
||||
|
const blockWidth = containerWidth / (colNum - 1); |
||||
|
const blockHeight = containerHeight / (rowNum - 1); |
||||
|
const elMap = new Map(); |
||||
|
Object.assign(this, { |
||||
|
time: 0, |
||||
|
index: 0, |
||||
|
items, |
||||
|
colNum, |
||||
|
rowNum, |
||||
|
elNum, |
||||
|
imgWidth, |
||||
|
imgHeight, |
||||
|
ratioSpeed, |
||||
|
rows: [], |
||||
|
key: 0, |
||||
|
blockWidth, |
||||
|
blockHeight, |
||||
|
elMap, |
||||
|
onListChange, |
||||
|
maxDeltaTime: 1 / 30, |
||||
|
}); |
||||
|
} |
||||
|
getNext() { |
||||
|
const { index, elNum, items } = this; |
||||
|
if (index === elNum) { |
||||
|
this.index = 1; |
||||
|
return items[0]; |
||||
|
} else { |
||||
|
this.index++; |
||||
|
return items[index]; |
||||
|
} |
||||
|
} |
||||
|
init() { |
||||
|
const { rowNum } = this; |
||||
|
|
||||
|
for (let i = 0; i < rowNum; i++) { |
||||
|
const row = new Row({ wall: this, index: i }); |
||||
|
row.init(); |
||||
|
this.rows.push(row); |
||||
|
} |
||||
|
this.#rafID = window.requestAnimationFrame(this.animate); |
||||
|
this.isRunning = true; |
||||
|
return this; |
||||
|
} |
||||
|
animate = () => { |
||||
|
if (!this.isRunning) return; |
||||
|
window.requestAnimationFrame(this.animate); |
||||
|
|
||||
|
const now = performance.now(); |
||||
|
const dt = Math.min(this.maxDeltaTime, (now - this.#lastTime) / 1000); |
||||
|
this.rows.forEach((row) => row.nextFrame(this.ratioSpeed)); |
||||
|
this.getList(); |
||||
|
this.time += dt; |
||||
|
this.#lastTime = now; |
||||
|
}; |
||||
|
dispose() { |
||||
|
if (this.#rafID === null) return; |
||||
|
window.cancelAnimationFrame(this.#rafID); |
||||
|
this.#rafID = null; |
||||
|
this.isRunning = false; |
||||
|
return this; |
||||
|
} |
||||
|
getKey() { |
||||
|
this.key++; |
||||
|
return this.key; |
||||
|
} |
||||
|
getList() { |
||||
|
if (this.onListChange) this.onListChange(Array.from(this.elMap.entries())); |
||||
|
} |
||||
|
attachSphere(sphere) { |
||||
|
this.sphere = sphere; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue