mirror of
https://github.com/decompme/decomp.me.git
synced 2025-12-20 04:19:42 -06:00
Implement colorful frog avatars for anonymous users (#517)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
from pathlib import Path
|
||||
import json
|
||||
import random
|
||||
@@ -50,6 +50,19 @@ class Profile(models.Model):
|
||||
# No URLs for anonymous profiles
|
||||
return None
|
||||
|
||||
def get_frog_color(self) -> Tuple[float, float, float]:
|
||||
"""Use the ID of this profile to generate a random color for use in a frog profile picture"""
|
||||
prev_state = random.getstate()
|
||||
random.seed(self.id)
|
||||
|
||||
hue = random.uniform(0, 360)
|
||||
satuation = random.uniform(0.35, 0.65)
|
||||
lightness = random.uniform(0.35, 0.65)
|
||||
|
||||
random.setstate(prev_state)
|
||||
|
||||
return (hue, satuation, lightness)
|
||||
|
||||
def is_online(self) -> bool:
|
||||
delta = timezone.now() - self.last_request_date
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ def serialize_profile(
|
||||
"id": profile.id,
|
||||
"is_online": profile.is_online(),
|
||||
"username": f"{profile.pseudonym} (anon)",
|
||||
"frog_color": profile.get_frog_color(),
|
||||
}
|
||||
else:
|
||||
user = profile.user
|
||||
|
||||
@@ -13,6 +13,7 @@ import useSWR from "swr"
|
||||
import * as api from "../../lib/api"
|
||||
import LoadingSpinner from "../loading.svg"
|
||||
import ScratchIcon from "../ScratchIcon"
|
||||
import AnonymousFrogAvatar from "../user/AnonymousFrog"
|
||||
import verticalMenuStyles from "../VerticalMenu.module.scss" // eslint-disable-line css-modules/no-unused-class
|
||||
|
||||
import styles from "./Search.module.scss"
|
||||
@@ -169,13 +170,20 @@ function MountedSearch({ className }: { className?: string }) {
|
||||
<span className={styles.itemName}>
|
||||
{scratch.name}
|
||||
</span>
|
||||
{scratch.owner && !api.isAnonUser(scratch.owner) && scratch.owner.avatar_url && <Image
|
||||
src={scratch.owner.avatar_url}
|
||||
alt={scratch.owner.username}
|
||||
width={16}
|
||||
height={16}
|
||||
className={styles.scratchOwnerAvatar}
|
||||
/>}
|
||||
{scratch.owner && (!api.isAnonUser(scratch.owner) ?
|
||||
scratch.owner.avatar_url && <Image
|
||||
src={scratch.owner.avatar_url}
|
||||
alt={scratch.owner.username}
|
||||
width={16}
|
||||
height={16}
|
||||
className={styles.scratchOwnerAvatar}
|
||||
/> :
|
||||
<AnonymousFrogAvatar
|
||||
user={scratch.owner}
|
||||
width={16}
|
||||
height={16}
|
||||
className={styles.scratchOwnerAvatar}
|
||||
/>)}
|
||||
</a>
|
||||
</li>
|
||||
})}
|
||||
|
||||
27
frontend/src/components/user/AnonymousFrog.module.scss
Normal file
27
frontend/src/components/user/AnonymousFrog.module.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.anonymousFrog {
|
||||
/* Default: decomp.me purple */
|
||||
--accent-hue: 298;
|
||||
--accent-saturation: 0.75;
|
||||
--accent-lightness: 0.4862;
|
||||
--frog-pupil: #292f33;
|
||||
--frog-primary:
|
||||
hsl(
|
||||
var(--accent-hue),
|
||||
calc(100% * var(--accent-saturation)),
|
||||
calc(100% * var(--accent-lightness))
|
||||
);
|
||||
|
||||
/* Pure CSS doesn't have anything quite as slick as Sass' color.scale [as used in theme.scss]
|
||||
for creating variations of a color. As such, we have to do a bit of the maths ourselves. */
|
||||
--frog-secondary:
|
||||
hsl(
|
||||
var(--accent-hue),
|
||||
calc(100% * (0.3 * (1 - var(--accent-saturation)) + var(--accent-saturation))), /* 30% towards maximum saturation */
|
||||
calc(100% * (0.5 * (1 - var(--accent-lightness)) + var(--accent-lightness))) /* 50% towards maximum lightness */
|
||||
);
|
||||
--frog-nose:
|
||||
hsl(
|
||||
calc(100% * (-0.2 * (var(--accent-saturation)) + var(--accent-saturation))), /* 20% towards minimum saturation */
|
||||
calc(100% * (-0.4 * (var(--accent-lightness)) + var(--accent-lightness))) /* 40% towards minimum lightness */
|
||||
);
|
||||
}
|
||||
23
frontend/src/components/user/AnonymousFrog.tsx
Normal file
23
frontend/src/components/user/AnonymousFrog.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { SVGProps } from "react"
|
||||
|
||||
import classNames from "classnames"
|
||||
|
||||
import * as api from "../../lib/api"
|
||||
import Frog from "../Nav/frog.svg"
|
||||
|
||||
import styles from "./AnonymousFrog.module.scss"
|
||||
|
||||
export type Props = SVGProps<SVGElement> & {
|
||||
user: api.AnonymousUser
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function AnonymousFrogAvatar({ user, className, ...props }: Props) {
|
||||
const accentStyle = {
|
||||
"--accent-hue": user.frog_color[0],
|
||||
"--accent-saturation": user.frog_color[1],
|
||||
"--accent-lightness": user.frog_color[2],
|
||||
}
|
||||
|
||||
return <Frog style={accentStyle} className={classNames(styles.anonymousFrog, className)} {...props}/>
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import classNames from "classnames"
|
||||
|
||||
import * as api from "../../lib/api"
|
||||
|
||||
import AnonymousFrogAvatar from "./AnonymousFrog"
|
||||
import styles from "./UserAvatar.module.scss"
|
||||
|
||||
export type Props = {
|
||||
@@ -13,7 +14,7 @@ export type Props = {
|
||||
|
||||
export default function UserAvatar({ user, className }: Props) {
|
||||
return <span className={classNames(styles.avatar, className)}>
|
||||
{!api.isAnonUser(user) && user.avatar_url && <Image src={user.avatar_url} alt="" layout="fill" />}
|
||||
{api.isAnonUser(user) ? <AnonymousFrogAvatar user={user}/> : user.avatar_url && <Image src={user.avatar_url} alt="" layout="fill" />}
|
||||
{user.is_online && <div className={styles.online} title="Online" />}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -181,6 +181,8 @@ export interface AnonymousUser {
|
||||
id: number
|
||||
is_online: boolean
|
||||
username: string
|
||||
|
||||
frog_color: [number, number, number]
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
||||
Reference in New Issue
Block a user