Implement colorful frog avatars for anonymous users (#517)

This commit is contained in:
ConorB
2022-09-03 11:21:38 +01:00
committed by GitHub
parent fa9bab2ec6
commit b026a2ef10
7 changed files with 84 additions and 9 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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>
})}

View 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 */
);
}

View 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}/>
}

View File

@@ -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>
}

View File

@@ -181,6 +181,8 @@ export interface AnonymousUser {
id: number
is_online: boolean
username: string
frog_color: [number, number, number]
}
export interface User {