WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 3070e47

Browse files
committed
count frames
1 parent 5dde2cd commit 3070e47

File tree

3 files changed

+64
-21
lines changed

3 files changed

+64
-21
lines changed

src/pages/suika/index.tsx

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import styles from './styles.module.css'
22
import { Head } from "#components/Head"
33
import type { RouteMeta } from "#router"
44
import { useEffect, useRef, useState } from "react"
5+
import { makeFrameCounter } from "#components/makeFrameCounter"
56

67
export const meta: RouteMeta = {
78
title: 'Suika Game',
@@ -11,6 +12,8 @@ export const meta: RouteMeta = {
1112
export default function SuikaGamePage() {
1213
const canvasRef = useRef<HTMLCanvasElement>(null)
1314
const [score, setScore] = useState(0)
15+
const [ups, setUps] = useState('')
16+
const [fps, setFps] = useState('')
1417

1518
useEffect(() => {
1619
const canvas = canvasRef.current!
@@ -24,7 +27,21 @@ export default function SuikaGamePage() {
2427
ctx.scale(devicePixelRatio, devicePixelRatio)
2528

2629
const controller = new AbortController()
27-
start(controller.signal, ctx, setScore)
30+
const updateCounter = makeFrameCounter(200)
31+
const frameCounter = makeFrameCounter(60)
32+
33+
const formatter = new Intl.NumberFormat('en-US', {
34+
maximumFractionDigits: 1,
35+
minimumFractionDigits: 1,
36+
})
37+
38+
start({
39+
signal: controller.signal,
40+
ctx,
41+
setScore,
42+
onUpdate: (dt) => setUps(formatter.format(updateCounter(dt))),
43+
onFrame: (dt) => setFps(formatter.format(frameCounter(dt)))
44+
})
2845

2946
return () => {
3047
controller.abort()
@@ -35,6 +52,8 @@ export default function SuikaGamePage() {
3552
<div className={styles.main}>
3653
<div className={styles.head}>
3754
<Head />
55+
<output>UPS: {ups}</output>
56+
<output>FPS: {fps}</output>
3857
<output>Score: {score}</output>
3958
</div>
4059
<canvas ref={canvasRef} className={styles.canvas} />
@@ -44,7 +63,7 @@ export default function SuikaGamePage() {
4463

4564
/**
4665
* When 2 entities of the same level (index) touch,
47-
* they fuse in 1 single entity of the next level.
66+
* they fuse into 1 single entity of the next level.
4867
*
4968
* The new entity is created at the position of the touch,
5069
* with velocity being the average of the 2 original entities.
@@ -60,7 +79,7 @@ const CHAIN = [
6079
{ r: 60, color: '#118ab2', score: 11 },
6180
{ r: 70, color: '#7209b7', score: 20 },
6281
{ r: 80, color: '#d90429', score: 50 },
63-
{ r: 90, color: '#ef23efff', score: 100 },
82+
{ r: 90, color: '#ef23ef', score: 100 },
6483
{ r: 100, color: '#ffd60a', score: 200 },
6584
{ r: 110, color: '#003566', score: 500 },
6685
{ r: 120, color: '#3f0139', score: 1000 },
@@ -75,18 +94,31 @@ type Entity = {
7594
y: number
7695
vx: number
7796
vy: number
97+
/** r**3 */
98+
mass: number
7899
}
79100

80-
function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (update: (prev: number) => number) => void) {
101+
function start({
102+
signal,
103+
ctx,
104+
setScore,
105+
onUpdate,
106+
onFrame,
107+
}: {
108+
signal: AbortSignal,
109+
ctx: CanvasRenderingContext2D,
110+
setScore: (update: (prev: number) => number) => void
111+
onUpdate: (dt: number) => void
112+
onFrame: (dt: number) => void
113+
}) {
81114
/** All entities currently in the game */
82115
const entities: Entity[] = []
83116
/** the maximum level (index) of entity present in the game */
84117
let max = 0
85-
let nextId = 0
86118
/** what will be dropped when the user clicks (index in CHAIN) */
87119
let handId = 0
88120
/** preview of the next handId (index in CHAIN), will become handId when the user clicks */
89-
121+
let nextId = 0
90122
/** position at which to drop the new entity (handId) on click */
91123
let mouseX = 0
92124

@@ -102,9 +134,10 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
102134
r: base.r,
103135
color: base.color,
104136
x,
105-
y: base.r,
137+
y: DROP_Y,
106138
vx: 0,
107139
vy: 0,
140+
mass: base.r ** 3,
108141
})
109142
handId = nextId
110143
nextId = Math.floor(Math.random() * max)
@@ -115,18 +148,23 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
115148
const CONTAINER_HEIGHT = 1000
116149
const containerX = ctx.canvas.width / devicePixelRatio / 2 - CONTAINER_WIDTH / 2
117150
const containerY = ctx.canvas.height / devicePixelRatio - CONTAINER_HEIGHT
151+
const DROP_Y = 50
118152

119-
let lastTime = performance.now()
153+
let lastTime = 0
120154
let rafId = requestAnimationFrame(function loop(time) {
121155
rafId = requestAnimationFrame(loop)
122-
const dt = (time - lastTime) / 16.6667
156+
const diff = time - lastTime
123157
lastTime = time
124-
if (dt > 1) return // skip frame if too much time has passed
158+
if (diff === time || diff > 1000) return
159+
onFrame(diff / 1000)
160+
const dt = diff / 16.6667
125161

126-
const steps = 20
162+
const steps = Math.ceil(dt / 0.1) * 3
163+
const timeStep = diff / 1000 / steps
164+
const dti = dt / steps
127165

128166
for (let step = 0; step < steps; step++) {
129-
const dti = dt / steps
167+
onUpdate(timeStep)
130168

131169
// Update entities
132170
for (const entity of entities) {
@@ -183,6 +221,7 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
183221
y: (a.y + b.y) / 2,
184222
vx: (a.vx + b.vx) / 2,
185223
vy: (a.vy + b.vy) / 2 - 7, // slight upward boost on merge
224+
mass: base.r ** 3,
186225
})
187226
if (newId < CHAIN.length - 2)
188227
max = Math.max(max, newId)
@@ -231,13 +270,11 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
231270

232271
if (speed > 0) continue // Objects separating
233272

234-
const massA = a.r ** 3
235-
const massB = b.r ** 3
236-
const impulse = 2 * speed / (massA + massB) * 0.8 // restitution coefficient
237-
a.vx -= impulse * massB * normalX
238-
a.vy -= impulse * massB * normalY
239-
b.vx += impulse * massA * normalX
240-
b.vy += impulse * massA * normalY
273+
const impulse = 2 * speed / (a.mass + b.mass) * 0.8 // restitution coefficient
274+
a.vx -= impulse * b.mass * normalX
275+
a.vy -= impulse * b.mass * normalY
276+
b.vx += impulse * a.mass * normalX
277+
b.vy += impulse * a.mass * normalY
241278
}
242279
}
243280
}
@@ -257,7 +294,7 @@ function start(signal: AbortSignal, ctx: CanvasRenderingContext2D, setScore: (up
257294
ctx.fillStyle = base.color
258295
ctx.beginPath()
259296
const x = Math.max(containerX + base.r, Math.min(containerX + CONTAINER_WIDTH - base.r, mouseX))
260-
ctx.arc(x, 50, base.r, 0, Math.PI * 2)
297+
ctx.arc(x, DROP_Y, base.r, 0, Math.PI * 2)
261298
ctx.fill()
262299
}
263300

src/pages/suika/styles.module.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
z-index: 0;
1818
pointer-events: none;
1919
}
20+
21+
output {
22+
display: block;
23+
font-family: monospace;
24+
font-variant-numeric: tabular-nums;
25+
}
2026
}
2127

2228
.head {

src/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export const ROUTES = {
128128
tags: ['game'],
129129
},
130130
git: {
131-
lastModified: 1764879699000,
131+
lastModified: 1764880844000,
132132
firstAdded: 1764878117000
133133
},
134134
},

0 commit comments

Comments
 (0)