pacman-contribution-graph 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/assets/packman-demo.png +0 -0
- package/dist/pacman-contribution-graph.js +372 -341
- package/dist/pacman-contribution-graph.js.map +1 -1
- package/dist/pacman-contribution-graph.min.js +1 -1
- package/embeded/canvas.html +41 -0
- package/embeded/svg.html +18 -0
- package/index.html +79 -47
- package/package.json +3 -2
- package/server/api/contributions/route.ts.z +31 -0
- package/server/page.zts.z +13 -0
- package/src/assets/images/ghosts/blinky.png +0 -0
- package/src/assets/images/ghosts/clyde.png +0 -0
- package/src/assets/images/ghosts/inky.png +0 -0
- package/src/assets/images/ghosts/pinky.png +0 -0
- package/src/assets/images/ghosts/scared.png +0 -0
- package/src/canvas.ts +82 -94
- package/src/constants.ts +29 -2
- package/src/game.ts +144 -121
- package/src/index.ts +38 -23
- package/src/music-player.ts +13 -7
- package/src/store.ts +3 -2
- package/src/svg.ts +69 -78
- package/src/types.ts +7 -4
- package/src/utils.ts +7 -5
package/src/svg.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { CELL_SIZE, GAP_SIZE, GRID_HEIGHT, GRID_WIDTH, PACMAN_COLOR, PACMAN_COLOR_DEAD, PACMAN_COLOR_POWERUP } from './constants';
|
|
2
|
-
import {
|
|
1
|
+
import { CELL_SIZE, GAP_SIZE, GHOSTS, GRID_HEIGHT, GRID_WIDTH, PACMAN_COLOR, PACMAN_COLOR_DEAD, PACMAN_COLOR_POWERUP } from './constants';
|
|
2
|
+
import { StoreType } from './types';
|
|
3
3
|
import { Utils } from './utils';
|
|
4
4
|
|
|
5
|
-
const generateAnimatedSVG = () => {
|
|
5
|
+
const generateAnimatedSVG = (store: StoreType) => {
|
|
6
6
|
const svgWidth = GRID_WIDTH * (CELL_SIZE + GAP_SIZE);
|
|
7
7
|
const svgHeight = GRID_HEIGHT * (CELL_SIZE + GAP_SIZE) + 20;
|
|
8
8
|
let svg = `<svg width="${svgWidth}" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg">`;
|
|
9
|
-
svg += `<rect width="100%" height="100%" fill="${Utils.getCurrentTheme().gridBackground}"/>`;
|
|
9
|
+
svg += `<rect width="100%" height="100%" fill="${Utils.getCurrentTheme(store).gridBackground}"/>`;
|
|
10
|
+
|
|
11
|
+
svg += generateGhostsPredefinition();
|
|
10
12
|
|
|
11
13
|
// Month labels
|
|
12
14
|
let lastMonth = '';
|
|
13
15
|
for (let y = 0; y < GRID_WIDTH; y++) {
|
|
14
|
-
if (
|
|
16
|
+
if (store.monthLabels[y] !== lastMonth) {
|
|
15
17
|
const xPos = y * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2;
|
|
16
|
-
svg += `<text x="${xPos}" y="10" text-anchor="middle" font-size="10" fill="${Utils.getCurrentTheme().textColor}">${
|
|
17
|
-
lastMonth =
|
|
18
|
+
svg += `<text x="${xPos}" y="10" text-anchor="middle" font-size="10" fill="${Utils.getCurrentTheme(store).textColor}">${store.monthLabels[y]}</text>`;
|
|
19
|
+
lastMonth = store.monthLabels[y];
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -23,62 +25,43 @@ const generateAnimatedSVG = () => {
|
|
|
23
25
|
for (let y = 0; y < GRID_WIDTH; y++) {
|
|
24
26
|
const cellX = y * (CELL_SIZE + GAP_SIZE);
|
|
25
27
|
const cellY = x * (CELL_SIZE + GAP_SIZE) + 15;
|
|
26
|
-
const intensity =
|
|
27
|
-
const color = intensity > 0 ? getContributionColor(intensity) : Utils.getCurrentTheme().emptyContributionBoxColor;
|
|
28
|
+
const intensity = store.gameHistory[0].grid[x][y];
|
|
29
|
+
const color = intensity > 0 ? getContributionColor(store, intensity) : Utils.getCurrentTheme(store).emptyContributionBoxColor;
|
|
28
30
|
svg += `<rect id="cell-${x}-${y}" x="${cellX}" y="${cellY}" width="${CELL_SIZE}" height="${CELL_SIZE}" rx="5" fill="${color}">
|
|
29
|
-
<animate attributeName="fill" dur="${
|
|
30
|
-
values="${generateCellColorValues(x, y)}"
|
|
31
|
-
keyTimes="${generateKeyTimes()}"/>
|
|
31
|
+
<animate attributeName="fill" dur="${store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
32
|
+
values="${generateCellColorValues(store, x, y)}"
|
|
33
|
+
keyTimes="${generateKeyTimes(store)}"/>
|
|
32
34
|
</rect>`;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// Pacman
|
|
37
39
|
svg += `<path id="pacman" d="${generatePacManPath(0.35)}"
|
|
38
|
-
transform="translate(${
|
|
39
|
-
<animate attributeName="fill" dur="${
|
|
40
|
-
keyTimes="${generateKeyTimes()}"
|
|
41
|
-
values="${generatePacManColors()}"/>
|
|
42
|
-
<animateTransform attributeName="transform" type="translate" dur="${
|
|
43
|
-
keyTimes="${generateKeyTimes()}"
|
|
44
|
-
values="${generatePacManPositions()}"/>
|
|
40
|
+
transform="translate(${store.pacman.y * (CELL_SIZE + GAP_SIZE)}, ${store.pacman.x * (CELL_SIZE + GAP_SIZE) + 15})">
|
|
41
|
+
<animate attributeName="fill" dur="${store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
42
|
+
keyTimes="${generateKeyTimes(store)}"
|
|
43
|
+
values="${generatePacManColors(store)}"/>
|
|
44
|
+
<animateTransform attributeName="transform" type="translate" dur="${store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
45
|
+
keyTimes="${generateKeyTimes(store)}"
|
|
46
|
+
values="${generatePacManPositions(store)}"/>
|
|
45
47
|
<animate attributeName="d" dur="0.2s" repeatCount="indefinite"
|
|
46
48
|
values="${generatePacManPath(0.25)};${generatePacManPath(0.05)};${generatePacManPath(0.25)}"/>
|
|
47
49
|
</path>`;
|
|
48
50
|
|
|
49
51
|
// Ghosts
|
|
50
|
-
|
|
51
|
-
svg += `<
|
|
52
|
-
<
|
|
53
|
-
keyTimes="${generateKeyTimes()}"
|
|
54
|
-
values="${
|
|
55
|
-
<
|
|
56
|
-
keyTimes="${generateKeyTimes()}"
|
|
57
|
-
values="${
|
|
58
|
-
</
|
|
59
|
-
<circle cx="${CELL_SIZE / 3}" cy="${CELL_SIZE / 3}" r="${CELL_SIZE / 8}" fill="white">
|
|
60
|
-
<animateTransform attributeName="transform" type="translate" dur="${Store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
61
|
-
keyTimes="${generateKeyTimes()}"
|
|
62
|
-
values="${generateGhostPositions(index)}"/>
|
|
63
|
-
</circle>
|
|
64
|
-
<circle cx="${(CELL_SIZE * 2) / 3}" cy="${CELL_SIZE / 3}" r="${CELL_SIZE / 8}" fill="white">
|
|
65
|
-
<animateTransform attributeName="transform" type="translate" dur="${Store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
66
|
-
keyTimes="${generateKeyTimes()}"
|
|
67
|
-
values="${generateGhostPositions(index)}"/>
|
|
68
|
-
</circle>
|
|
69
|
-
<circle cx="${CELL_SIZE / 3}" cy="${CELL_SIZE / 3}" r="${CELL_SIZE / 16}" fill="black">
|
|
70
|
-
<animateTransform attributeName="transform" type="translate" dur="${Store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
71
|
-
keyTimes="${generateKeyTimes()}"
|
|
72
|
-
values="${generateGhostPositions(index)}"/>
|
|
73
|
-
</circle>
|
|
74
|
-
<circle cx="${(CELL_SIZE * 2) / 3}" cy="${CELL_SIZE / 3}" r="${CELL_SIZE / 16}" fill="black">
|
|
75
|
-
<animateTransform attributeName="transform" type="translate" dur="${Store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
76
|
-
keyTimes="${generateKeyTimes()}"
|
|
77
|
-
values="${generateGhostPositions(index)}"/>
|
|
78
|
-
</circle>`;
|
|
52
|
+
store.ghosts.forEach((ghost, index) => {
|
|
53
|
+
svg += `<use id="ghost${index}" width="${CELL_SIZE}" height="${CELL_SIZE}" href="#ghost-${ghost.name}">
|
|
54
|
+
<animateTransform attributeName="transform" type="translate" dur="${store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
55
|
+
keyTimes="${generateKeyTimes(store)}"
|
|
56
|
+
values="${generateGhostPositions(store, index)}"/>
|
|
57
|
+
<animate attributeName="href" dur="${store.gameHistory.length * 300}ms" repeatCount="indefinite"
|
|
58
|
+
keyTimes="${generateKeyTimes(store)}"
|
|
59
|
+
values="${generateGhostColors(store, index)}"/>
|
|
60
|
+
</use>`;
|
|
79
61
|
});
|
|
80
62
|
|
|
81
63
|
svg += '</svg>';
|
|
64
|
+
// TODO: minify SVG
|
|
82
65
|
return svg;
|
|
83
66
|
};
|
|
84
67
|
|
|
@@ -93,12 +76,12 @@ const generatePacManPath = (mouthAngle: number) => {
|
|
|
93
76
|
Z`;
|
|
94
77
|
};
|
|
95
78
|
|
|
96
|
-
const generateKeyTimes = () => {
|
|
97
|
-
return
|
|
79
|
+
const generateKeyTimes = (store: StoreType) => {
|
|
80
|
+
return store.gameHistory.map((_, index) => index / (store.gameHistory.length - 1)).join(';');
|
|
98
81
|
};
|
|
99
82
|
|
|
100
|
-
const generatePacManPositions = () => {
|
|
101
|
-
return
|
|
83
|
+
const generatePacManPositions = (store: StoreType) => {
|
|
84
|
+
return store.gameHistory
|
|
102
85
|
.map((state) => {
|
|
103
86
|
const x = state.pacman.y * (CELL_SIZE + GAP_SIZE);
|
|
104
87
|
const y = state.pacman.x * (CELL_SIZE + GAP_SIZE) + 15;
|
|
@@ -107,12 +90,12 @@ const generatePacManPositions = () => {
|
|
|
107
90
|
.join(';');
|
|
108
91
|
};
|
|
109
92
|
|
|
110
|
-
const generatePacManColors = () => {
|
|
111
|
-
return
|
|
93
|
+
const generatePacManColors = (store: StoreType) => {
|
|
94
|
+
return store.gameHistory
|
|
112
95
|
.map((state) => {
|
|
113
|
-
if (state.pacman.
|
|
96
|
+
if (state.pacman.deadRemainingDuration) {
|
|
114
97
|
return PACMAN_COLOR_DEAD;
|
|
115
|
-
} else if (state.pacman.
|
|
98
|
+
} else if (state.pacman.powerupRemainingDuration) {
|
|
116
99
|
return PACMAN_COLOR_POWERUP;
|
|
117
100
|
} else {
|
|
118
101
|
return PACMAN_COLOR;
|
|
@@ -121,22 +104,22 @@ const generatePacManColors = () => {
|
|
|
121
104
|
.join(';');
|
|
122
105
|
};
|
|
123
106
|
|
|
124
|
-
const generateCellColorValues = (x: number, y: number) => {
|
|
125
|
-
return
|
|
107
|
+
const generateCellColorValues = (store: StoreType, x: number, y: number) => {
|
|
108
|
+
return store.gameHistory
|
|
126
109
|
.map((state) => {
|
|
127
110
|
const intensity = state.grid[x][y];
|
|
128
|
-
return intensity > 0 ? getContributionColor(intensity) : Utils.getCurrentTheme().emptyContributionBoxColor;
|
|
111
|
+
return intensity > 0 ? getContributionColor(store, intensity) : Utils.getCurrentTheme(store).emptyContributionBoxColor;
|
|
129
112
|
})
|
|
130
113
|
.join(';');
|
|
131
114
|
};
|
|
132
115
|
|
|
133
|
-
const getContributionColor = (intensity: number) => {
|
|
116
|
+
const getContributionColor = (store: StoreType, intensity: number) => {
|
|
134
117
|
const adjustedIntensity = intensity < 0.2 ? 0.3 : intensity;
|
|
135
|
-
return Utils.hexToRGBA(Utils.getCurrentTheme().contributionBoxColor, adjustedIntensity);
|
|
118
|
+
return Utils.hexToRGBA(Utils.getCurrentTheme(store).contributionBoxColor, adjustedIntensity);
|
|
136
119
|
};
|
|
137
120
|
|
|
138
|
-
const generateGhostPositions = (ghostIndex: number) => {
|
|
139
|
-
return
|
|
121
|
+
const generateGhostPositions = (store: StoreType, ghostIndex: number) => {
|
|
122
|
+
return store.gameHistory
|
|
140
123
|
.map((state) => {
|
|
141
124
|
const ghost = state.ghosts[ghostIndex];
|
|
142
125
|
const x = ghost.y * (CELL_SIZE + GAP_SIZE);
|
|
@@ -146,27 +129,35 @@ const generateGhostPositions = (ghostIndex: number) => {
|
|
|
146
129
|
.join(';');
|
|
147
130
|
};
|
|
148
131
|
|
|
149
|
-
const
|
|
150
|
-
return
|
|
151
|
-
Q ${radius * 0.8},${radius * 1.5} ${radius * 0.5},${radius * 1.3}
|
|
152
|
-
Q ${radius * 0.3},${radius * 1.1} 0,${radius}
|
|
153
|
-
L 0,0
|
|
154
|
-
L ${radius * 2},0
|
|
155
|
-
L ${radius * 2},${radius}
|
|
156
|
-
Q ${radius * 1.7},${radius * 1.1} ${radius * 1.5},${radius * 1.3}
|
|
157
|
-
Q ${radius * 1.2},${radius * 1.5} ${radius},${radius * 2}
|
|
158
|
-
Z`;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const generateGhostColors = (ghostIndex: number) => {
|
|
162
|
-
return Store.gameHistory
|
|
132
|
+
const generateGhostColors = (store: StoreType, ghostIndex: number) => {
|
|
133
|
+
return store.gameHistory
|
|
163
134
|
.map((state) => {
|
|
164
135
|
const ghost = state.ghosts[ghostIndex];
|
|
165
|
-
return ghost.scared ? '
|
|
136
|
+
return ghost.scared ? '#scared' : '#' + ghost.name;
|
|
166
137
|
})
|
|
167
138
|
.join(';');
|
|
168
139
|
};
|
|
169
140
|
|
|
141
|
+
const generateGhostsPredefinition = () => {
|
|
142
|
+
return `<defs>
|
|
143
|
+
<symbol id="blinky" viewBox="0 0 100 100">
|
|
144
|
+
<image href="${GHOSTS['blinky'].imgDate}" width="100" height="100"/>
|
|
145
|
+
</symbol>
|
|
146
|
+
<symbol id="clyde" viewBox="0 0 100 100">
|
|
147
|
+
<image href="${GHOSTS['clyde'].imgDate}" width="100" height="100"/>
|
|
148
|
+
</symbol>
|
|
149
|
+
<symbol id="inky" viewBox="0 0 100 100">
|
|
150
|
+
<image href="${GHOSTS['inky'].imgDate}" width="100" height="100"/>
|
|
151
|
+
</symbol>
|
|
152
|
+
<symbol id="pinky" viewBox="0 0 100 100">
|
|
153
|
+
<image href="${GHOSTS['pinky'].imgDate}" width="100" height="100"/>
|
|
154
|
+
</symbol>
|
|
155
|
+
<symbol id="scared" viewBox="0 0 100 100">
|
|
156
|
+
<image href="${GHOSTS['scared'].imgDate}" width="100" height="100"/>
|
|
157
|
+
</symbol>
|
|
158
|
+
</defs>`;
|
|
159
|
+
};
|
|
160
|
+
|
|
170
161
|
export const SVG = {
|
|
171
162
|
generateAnimatedSVG
|
|
172
163
|
};
|
package/src/types.ts
CHANGED
|
@@ -3,14 +3,16 @@ export interface Pacman {
|
|
|
3
3
|
y: number;
|
|
4
4
|
direction: string;
|
|
5
5
|
points: number;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
totalPoints: number;
|
|
7
|
+
deadRemainingDuration: number;
|
|
8
|
+
powerupRemainingDuration: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
export type GhostName = 'blinky' | 'clyde' | 'inky' | 'pinky';
|
|
10
12
|
export interface Ghost {
|
|
11
13
|
x: number;
|
|
12
14
|
y: number;
|
|
13
|
-
|
|
15
|
+
name: GhostName;
|
|
14
16
|
scared: boolean;
|
|
15
17
|
target?: { x: number; y: number };
|
|
16
18
|
}
|
|
@@ -25,7 +27,7 @@ export interface StoreType {
|
|
|
25
27
|
contributions: Contribution[];
|
|
26
28
|
pacman: Pacman;
|
|
27
29
|
ghosts: Ghost[];
|
|
28
|
-
grid: number[][];
|
|
30
|
+
grid: { intensity: number; commitsCount: number }[][];
|
|
29
31
|
monthLabels: string[];
|
|
30
32
|
pacmanMouthOpen: boolean;
|
|
31
33
|
gameInterval: number;
|
|
@@ -48,6 +50,7 @@ export interface Config {
|
|
|
48
50
|
gameTheme: ThemeKeys;
|
|
49
51
|
gameSpeed: number;
|
|
50
52
|
enableSounds: boolean;
|
|
53
|
+
pointsIncreasedCallback: (pointsSum: number) => void;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export type ThemeKeys = 'github' | 'github-dark' | 'gitlab' | 'gitlab-dark';
|
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { GAME_THEMES } from './constants';
|
|
2
|
-
import {
|
|
3
|
-
import type { Contribution, GameTheme } from './types';
|
|
2
|
+
import type { Contribution, GameTheme, StoreType } from './types';
|
|
4
3
|
|
|
5
4
|
const getGitlabContribution = async (username: string): Promise<Contribution[]> => {
|
|
6
|
-
const response = await fetch(`https://gitlab.com/users/${username}/calendar.json`);
|
|
5
|
+
// const response = await fetch(`https://gitlab.com/users/${username}/calendar.json`);
|
|
6
|
+
const response = await fetch(
|
|
7
|
+
`https://v0-new-project-q1hhrdodoye-abozanona-gmailcoms-projects.vercel.app/api/contributions?username=${username}`
|
|
8
|
+
);
|
|
7
9
|
const contributionsList = await response.json();
|
|
8
10
|
return Object.entries(contributionsList).map(([date, count]) => ({
|
|
9
11
|
date: new Date(date),
|
|
@@ -28,8 +30,8 @@ const getGithubContribution = async (username: string): Promise<Contribution[]>
|
|
|
28
30
|
);
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
const getCurrentTheme = (): GameTheme => {
|
|
32
|
-
return GAME_THEMES[
|
|
33
|
+
const getCurrentTheme = (store: StoreType): GameTheme => {
|
|
34
|
+
return GAME_THEMES[store.config.gameTheme] ?? GAME_THEMES['github'];
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
function hexToRGBA(hex: string, alpha: number): string {
|