feat: use ualy avatar

This commit is contained in:
molvqingtai 2024-09-19 03:41:07 +08:00
parent ad2278f5ba
commit 89e20a65db
23 changed files with 1718 additions and 482 deletions

View file

@ -24,7 +24,7 @@ export default [
}
},
{
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**']
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**', '**/lib/**']
},
{
rules: {

View file

@ -63,11 +63,11 @@
"idb-keyval": "^6.2.1",
"lucide-react": "^0.350.0",
"nanoid": "^5.0.6",
"next-themes": "^0.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.0",
"react-markdown": "^9.0.1",
"react-nice-avatar": "^1.5.0",
"react-use": "^17.5.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
@ -75,7 +75,7 @@
"remesh-logger": "^4.1.0",
"remesh-react": "^4.1.2",
"rxjs": "^7.8.1",
"sonner": "^1.4.3",
"sonner": "^1.5.0",
"tailwind-merge": "^2.2.1",
"trystero": "^0.20.0",
"type-fest": "^4.11.1",

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,19 @@ import Footer from '@/app/content/views/Footer'
import Main from '@/app/content/views/Main'
import AppButton from '@/app/content/views/AppButton'
import AppContainer from '@/app/content/views/AppContainer'
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
import RoomDomain from '@/domain/Room'
import { stringToHex } from '@/utils'
import { Toaster } from '@/components/ui/Sonner'
const hostRoomId = stringToHex(document.location.host)
export default function App() {
const send = useRemeshSend()
const roomDomain = useRemeshDomain(RoomDomain())
send(roomDomain.command.JoinRoomCommand(hostRoomId))
return (
<>
<AppContainer>
@ -13,6 +24,7 @@ export default function App() {
<Footer />
</AppContainer>
<AppButton></AppButton>
<Toaster richColors offset="104px" position="top-center"></Toaster>
</>
)
}

View file

@ -10,6 +10,7 @@ import App from './App'
import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
import '@/assets/styles/tailwind.css'
import { createElement } from '@/utils'
export default defineContentScript({
cssInjectionMode: 'ui',
@ -26,8 +27,7 @@ export default defineContentScript({
// anchor: 'body',
// append: 'first',
onMount: (container) => {
const app = document.createElement('div')
app.id = 'app'
const app = createElement('<div id="app"></div>')
container.append(app)
const root = createRoot(app)

View file

@ -11,6 +11,9 @@ const Header: FC = () => {
const siteInfo = getSiteInfo()
const roomDomain = useRemeshDomain(RoomDomain())
const peerList = useRemeshQuery(roomDomain.query.PeerListQuery())
// const peerList = ['1', '2', '3']
console.log('peerList', peerList)
console.log(111)
return (
<div className="z-10 grid h-12 grid-flow-col items-center justify-between gap-x-4 rounded-t-xl bg-white px-4 backdrop-blur-lg">

View file

@ -3,7 +3,6 @@ import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import MessageList from '../../components/MessageList'
import MessageItem from '../../components/MessageItem'
import MessageListDomain from '@/domain/MessageList'
import UserInfoDomain from '@/domain/UserInfo'
import RoomDomain from '@/domain/Room'
@ -11,8 +10,7 @@ const Main: FC = () => {
const send = useRemeshSend()
const roomDomain = useRemeshDomain(RoomDomain())
const userInfoDomain = useRemeshDomain(UserInfoDomain())
const messageListDomain = useRemeshDomain(MessageListDomain())
const _messageList = useRemeshQuery(messageListDomain.query.ListQuery())
const _messageList = useRemeshQuery(roomDomain.query.MessageListQuery())
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
const messageList = _messageList.map((message) => ({
...message,

View file

@ -10,7 +10,7 @@ export interface AvatarSelectProps {
className?: string
disabled?: boolean
compressSize?: number
onSuccess?: (blob: Blob) => void
onSuccess?: (blob: string) => void
onWarning?: (error: Error) => void
onError?: (error: Error) => void
onChange?: (src: string) => void
@ -34,8 +34,9 @@ const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
const blob = await compressImage(file, compressSize)
const reader = new FileReader()
reader.onload = (e) => {
onSuccess?.(blob)
onChange?.(e.target?.result as string)
const base64 = e.target?.result as string
onSuccess?.(base64)
onChange?.(base64)
}
reader.onerror = () => onError?.(new Error('Failed to read image file.'))
reader.readAsDataURL(blob)

View file

@ -10,13 +10,15 @@ import { Button } from '@/components/ui/Button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
import { Input } from '@/components/ui/Input'
import UserInfoDomain, { type UserInfo } from '@/domain/UserInfo'
import { checkSystemDarkMode } from '@/utils'
import { checkSystemDarkMode, compressImage } from '@/utils'
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
import { Label } from '@/components/ui/Label'
import { RefreshCcwIcon } from 'lucide-react'
import generateUglyAvatar from '@/lib/uglyAvatar'
// In chrome storage.sync, each key-value pair supports a maximum storage of 8kb
// Image is encoded as base64, and the size is increased by about 33%.
const COMPRESS_SIZE = 8 * 1024 - 8 * 1024 * 0.33
const COMPRESS_SIZE = 8 * 1024 * (1 - 0.33)
const defaultUserInfo: UserInfo = {
id: nanoid(),
@ -80,6 +82,34 @@ const ProfileForm = () => {
toast.error(error.message)
}
const handleRandomAvatar = async () => {
const svgBlob = generateUglyAvatar()
// compressImage can't directly compress svg, need to convert to jpeg first
const jpegBlob = await new Promise<Blob>((resolve, reject) => {
const image = new Image()
image.onload = async () => {
const canvas = new OffscreenCanvas(image.width, image.height)
const ctx = canvas.getContext('2d')
ctx?.drawImage(image, 0, 0)
const blob = await canvas.convertToBlob({ type: 'image/jpeg' })
resolve(blob)
}
image.onerror = () => reject(new Error('Failed to load SVG'))
image.src = URL.createObjectURL(svgBlob)
})
const miniAvatarBlob = await compressImage(jpegBlob, COMPRESS_SIZE)
const miniAvatarBase64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => resolve(e.target?.result as string)
reader.onerror = () => reject(new Error('Failed to convert Blob to Base64'))
reader.readAsDataURL(miniAvatarBlob)
})
console.log('kb', miniAvatarBase64.length / 1024)
form.setValue('avatar', miniAvatarBase64)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} autoComplete="off" className="relative w-96 space-y-8 p-10">
@ -89,18 +119,30 @@ const ProfileForm = () => {
render={({ field }) => (
<FormItem className="absolute left-1/2 top-0 grid -translate-x-1/2 -translate-y-1/2 justify-items-center">
<FormControl>
<AvatarSelect
compressSize={COMPRESS_SIZE}
onError={handleError}
onWarning={handleWarning}
className="shadow-lg"
{...field}
></AvatarSelect>
<div className="grid justify-items-center gap-y-2">
<AvatarSelect
compressSize={COMPRESS_SIZE}
onError={handleError}
onWarning={handleWarning}
className="shadow-lg"
{...field}
></AvatarSelect>
<Button
type="button"
size="xs"
className="mx-auto flex items-center gap-x-2"
onClick={handleRandomAvatar}
>
<RefreshCcwIcon size={14} />
Random Avatar
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"

View file

@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View file

@ -4,7 +4,7 @@ import { type MessageUser } from './MessageList'
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
import MessageListDomain from '@/domain/MessageList'
import UserInfoDomain from '@/domain/UserInfo'
import { callbackToObservable, desert, stringToHex } from '@/utils'
import { callbackToObservable, desert } from '@/utils'
import { nanoid } from 'nanoid'
export enum MessageType {
@ -31,26 +31,41 @@ export interface TextMessage extends MessageUser {
export type RoomMessage = LikeMessage | HateMessage | TextMessage
const hostRoomId = stringToHex(document.location.host)
const RoomDomain = Remesh.domain({
name: 'RoomDomain',
impl: (domain) => {
const messageListDomain = domain.getDomain(MessageListDomain())
const userInfoDomain = domain.getDomain(UserInfoDomain())
const peerRoom = domain.getExtern(PeerRoomExtern)
peerRoom.joinRoom(hostRoomId)
const PeersListState = domain.state<string[]>({
name: 'Room.PeersListState',
const MessageListQuery = messageListDomain.query.ListQuery
const PeerListState = domain.state<string[]>({
name: 'Room.PeerListState',
default: [peerRoom.selfId]
})
const MessageListQuery = messageListDomain.query.ListQuery
const PeerListQuery = domain.query({
name: 'Room.PeerListQuery',
impl: ({ get }) => {
return get(PeersListState())
console.log('PeerListQuery')
return get(PeerListState())
}
})
const JoinRoomCommand = domain.command({
name: 'RoomJoinRoomCommand',
impl: ({ get }, roomId: string) => {
peerRoom.joinRoom(roomId)
return [JoinRoomEvent(peerRoom.selfId)]
}
})
const LeaveRoomCommand = domain.command({
name: 'RoomLeaveRoomCommand',
impl: ({ get }) => {
peerRoom.leaveRoom()
return [LeaveRoomEvent(peerRoom.selfId)]
}
})
@ -87,11 +102,15 @@ const RoomDomain = Remesh.domain({
return [
messageListDomain.command.UpdateItemCommand({
..._message,
likeUsers: desert(_message.likeUsers, 'userId', {
userId,
username,
userAvatar
})
likeUsers: desert(
_message.likeUsers,
{
userId,
username,
userAvatar
},
'userId'
)
}),
SendLikeMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Like })
]
@ -111,11 +130,15 @@ const RoomDomain = Remesh.domain({
return [
messageListDomain.command.UpdateItemCommand({
..._message,
hateUsers: desert(_message.hateUsers, 'userId', {
userId,
username,
userAvatar
})
hateUsers: desert(
_message.hateUsers,
{
userId,
username,
userAvatar
},
'userId'
)
}),
SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Hate })
]
@ -134,10 +157,24 @@ const RoomDomain = Remesh.domain({
name: 'RoomLeaveRoomEvent'
})
const SyncPeersListCommand = domain.command({
name: 'RoomSyncPeersListCommand',
impl: (_, list: string[]) => {
return [PeersListState().new(list)]
const UpdatePeerListCommand = domain.command({
name: 'RoomUpdatePeerListCommand',
impl: ({ get }, action: { type: 'create' | 'delete'; peerId: string }) => {
const peerList = get(PeerListState())
if (action.type === 'create') {
console.log('create', [...new Set(peerList).add(action.peerId)])
return [PeerListState().new([...new Set(peerList).add(action.peerId)])]
}
if (action.type === 'delete') {
console.log(
'delete',
peerList.filter((peerId) => peerId == action.peerId)
)
return [PeerListState().new(peerList.filter((peerId) => peerId == action.peerId))]
}
return null
}
})
@ -198,11 +235,15 @@ const RoomDomain = Remesh.domain({
const _message = get(messageListDomain.query.ItemQuery(message.id))
return messageListDomain.command.UpdateItemCommand({
..._message,
likeUsers: desert(_message.likeUsers, 'userId', {
userId: message.userId,
username: message.username,
userAvatar: message.userAvatar
})
likeUsers: desert(
_message.likeUsers,
{
userId: message.userId,
username: message.username,
userAvatar: message.userAvatar
},
'userId'
)
})
}
case 'hate': {
@ -212,11 +253,15 @@ const RoomDomain = Remesh.domain({
const _message = get(messageListDomain.query.ItemQuery(message.id))
return messageListDomain.command.UpdateItemCommand({
..._message,
hateUsers: desert(_message.hateUsers, 'userId', {
userId: message.userId,
username: message.username,
userAvatar: message.userAvatar
})
hateUsers: desert(
_message.hateUsers,
{
userId: message.userId,
username: message.username,
userAvatar: message.userAvatar
},
'userId'
)
})
}
default:
@ -230,11 +275,12 @@ const RoomDomain = Remesh.domain({
domain.effect({
name: 'RoomOnJoinRoomEffect',
impl: ({ get }) => {
impl: () => {
const onJoinRoom$ = callbackToObservable<string>(peerRoom.onJoinRoom.bind(peerRoom))
return onJoinRoom$.pipe(
map((peerId) => {
return [SyncPeersListCommand([...get(PeersListState()), peerId]), JoinRoomEvent(peerId)]
console.log('onJoinRoom', peerId)
return [UpdatePeerListCommand({ type: 'create', peerId }), JoinRoomEvent(peerId)]
})
)
}
@ -242,11 +288,12 @@ const RoomDomain = Remesh.domain({
domain.effect({
name: 'RoomOnLeaveRoomEffect',
impl: ({ get }) => {
impl: () => {
const onLeaveRoom$ = callbackToObservable<string>(peerRoom.onLeaveRoom.bind(peerRoom))
return onLeaveRoom$.pipe(
map((peerId) => {
return [SyncPeersListCommand(get(PeersListState()).filter((id) => id !== peerId)), LeaveRoomEvent(peerId)]
console.log('onLeaveRoom', peerId)
return [UpdatePeerListCommand({ type: 'delete', peerId }), LeaveRoomEvent(peerId)]
})
)
}
@ -264,6 +311,8 @@ const RoomDomain = Remesh.domain({
LeaveRoomEvent
},
command: {
JoinRoomCommand,
LeaveRoomCommand,
SendTextMessageCommand,
SendLikeMessageCommand,
SendHateMessageCommand

View file

@ -2,6 +2,8 @@ import { Remesh } from 'remesh'
import { nanoid } from 'nanoid'
import { BrowserSyncStorageExtern } from '@/domain/externs/Storage'
import StorageEffect from '@/domain/modules/StorageEffect'
import generateUglyAvatar from '@/lib/uglyAvatar'
import generateRandomName from '@/utils/generateRandomName'
export interface UserInfo {
id: string
@ -24,14 +26,7 @@ const UserInfoDomain = Remesh.domain({
const UserInfoState = domain.state<UserInfo | null>({
name: 'UserInfo.UserInfoState',
// defer: true
default: {
id: nanoid(),
name: '游客',
avatar: 'https://avatars.githubusercontent.com/u/10354233?v=4',
createTime: Date.now(),
themeMode: 'system'
}
default: null
})
const UserInfoQuery = domain.query({
@ -80,10 +75,10 @@ const UserInfoDomain = Remesh.domain({
}
})
// storageEffect
// .set(SyncToStorageEvent)
// .get<UserInfo>((value) => SyncToStateCommand(value))
// .watch<UserInfo>((value) => SyncToStateCommand(value))
storageEffect
.set(SyncToStorageEvent)
.get<UserInfo>((value) => SyncToStateCommand(value))
.watch<UserInfo>((value) => SyncToStateCommand(value!))
return {
query: {

View file

@ -21,12 +21,6 @@ class PeerRoom {
async joinRoom(roomId: string) {
this.room = joinRoom({ appId: this.appId }, roomId)
this.room?.onPeerJoin((peerId) => {
console.log(`${peerId} joined`)
console.log(this.room?.getPeers())
})
this.room?.onPeerLeave((peerId) => console.log(`${peerId} leaved`))
return this.room
}

View file

@ -0,0 +1,71 @@
import { type RemeshDomainContext, type DomainConceptName } from 'remesh'
import { toast } from 'sonner'
export interface ToastOptions {
name: DomainConceptName<'ToastModule'>
}
export const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => {
const SuccessEvent = domain.event({
name: `${options.name}.SuccessEvent`
})
const SuccessCommand = domain.command({
name: `${options.name}.SuccessCommand`,
impl: (_, message: string) => {
toast.success(message)
return [SuccessEvent()]
}
})
const ErrorEvent = domain.event({
name: `${options.name}.ErrorEvent`
})
const ErrorCommand = domain.command({
name: `${options.name}.ErrorCommand`,
impl: (_, message: string) => {
toast.error(message)
return [ErrorEvent()]
}
})
const InfoEvent = domain.event({
name: `${options.name}.InfoEvent`
})
const InfoCommand = domain.command({
name: `${options.name}.InfoCommand`,
impl: (_, message: string) => {
toast.info(message)
return [InfoEvent()]
}
})
const WarningEvent = domain.event({
name: `${options.name}.WarningEvent`
})
const WarningCommand = domain.command({
name: `${options.name}.WarningCommand`,
impl: (_, message: string) => {
toast.warning(message)
return [WarningEvent()]
}
})
return {
event: {
SuccessEvent,
ErrorEvent,
InfoEvent,
WarningEvent
},
command: {
SuccessCommand,
ErrorCommand,
InfoCommand,
WarningCommand
}
}
}

View file

@ -0,0 +1,163 @@
function randomFromInterval(min, max) {
// min and max included
return Math.random() * (max - min) + min;
}
function cubicBezier(P0, P1, P2, P3, t) {
var x = (1 - t) ** 3 * P0[0] + 3 * (1 - t) ** 2 * t * P1[0] + 3 * (1 - t) * t ** 2 * P2[0] + t ** 3 * P3[0];
var y = (1 - t) ** 3 * P0[1] + 3 * (1 - t) ** 2 * t * P1[1] + 3 * (1 - t) * t ** 2 * P2[1] + t ** 3 * P3[1];
return [x, y];
}
function generateEyeParameters(width) {
let height_upper = Math.random() * width / 1.2;// Less height for the upper eyelid to make it sharper
let height_lower = Math.random() * width / 1.2;// More height for the lower eyelid to make it rounder and droopier
let P0_upper_randX = Math.random() * 0.4 - 0.2;
let P3_upper_randX = Math.random() * 0.4 - 0.2;
let P0_upper_randY = Math.random() * 0.4 - 0.2;
let P3_upper_randY = Math.random() * 0.4 - 0.2;
let offset_upper_left_randY = Math.random();
let offset_upper_right_randY = Math.random();
let P0_upper = [-width / 2 + P0_upper_randX * width / 16, P0_upper_randY * height_upper / 16];
let P3_upper = [width / 2 + P3_upper_randX * width / 16, P3_upper_randY * height_upper / 16];
let P0_lower = P0_upper;// Starting at the same point as the upper eyelid
let P3_lower = P3_upper;// Ending at the same point as the upper eyelid
let eye_true_width = P3_upper[0] - P0_upper[0];
let offset_upper_left_x = randomFromInterval(-eye_true_width / 10.0, eye_true_width / 2.3);// Upper eyelid control point offset to create asymmetry
let offset_upper_right_x = randomFromInterval(-eye_true_width / 10.0, eye_true_width / 2.3);// Upper eyelid control point offset to create asymmetry
let offset_upper_left_y = offset_upper_left_randY * height_upper;// Upper eyelid control point offset to create asymmetry
let offset_upper_right_y = offset_upper_right_randY * height_upper;// Upper eyelid control point offset to create asymmetry
let offset_lower_left_x = randomFromInterval(offset_upper_left_x, eye_true_width / 2.1);// Lower eyelid control point offset
let offset_lower_right_x = randomFromInterval(offset_upper_right_x, eye_true_width / 2.1);// Upper eyelid control point offset to create asymmetry
let offset_lower_left_y = randomFromInterval(-offset_upper_left_y + 5, height_lower);// Upper eyelid control point offset to create asymmetry
let offset_lower_right_y = randomFromInterval(-offset_upper_right_y + 5, height_lower);// Upper eyelid control point offset to create asymmetry
// Generate points for the Bezier curves
let left_converge0 = Math.random();
let right_converge0 = Math.random();
// Generate points for the Bezier curves
let left_converge1 = Math.random();
let right_converge1 = Math.random();
return {
height_upper: height_upper,
height_lower: height_lower,
P0_upper_randX: P0_upper_randX,
P3_upper_randX: P3_upper_randX,
P0_upper_randY: P0_upper_randY,
P3_upper_randY: P3_upper_randY,
offset_upper_left_randY: offset_upper_left_randY,
offset_upper_right_randY: offset_upper_right_randY,
eye_true_width: eye_true_width,
offset_upper_left_x: offset_upper_left_x,
offset_upper_right_x: offset_upper_right_x,
offset_upper_left_y: offset_upper_left_y,
offset_upper_right_y: offset_upper_right_y,
offset_lower_left_x: offset_lower_left_x,
offset_lower_right_x: offset_lower_right_x,
offset_lower_left_y: offset_lower_left_y,
offset_lower_right_y: offset_lower_right_y,
left_converge0: left_converge0,
right_converge0: right_converge0,
left_converge1: left_converge1,
right_converge1: right_converge1
}
}
export function generateEyePoints(rands, width = 50) {
let P0_upper = [-width / 2 + rands.P0_upper_randX * width / 16, rands.P0_upper_randY * rands.height_upper / 16];
let P3_upper = [width / 2 + rands.P3_upper_randX * width / 16, rands.P3_upper_randY * rands.height_upper / 16];
let P0_lower = P0_upper;// Starting at the same point as the upper eyelid
let P3_lower = P3_upper;// Ending at the same point as the upper eyelid
let eye_true_width = P3_upper[0] - P0_upper[0];
// Upper eyelid control points
let P1_upper = [P0_upper[0] + rands.offset_upper_left_x, P0_upper[1] + rands.offset_upper_left_y]; // First control point
let P2_upper = [P3_upper[0] - rands.offset_upper_right_x, P3_upper[1] + rands.offset_upper_right_y]; // Second control point
// Lower eyelid control points
let P1_lower = [P0_lower[0] + rands.offset_lower_left_x, P0_lower[1] - rands.offset_lower_left_y]; // First control point
let P2_lower = [P3_lower[0] - rands.offset_lower_right_x, P3_lower[1] - rands.offset_lower_right_y]; // Second control point
// now we generate the points for the upper eyelid
let upper_eyelid_points = [];
let upper_eyelid_points_left_control = [];
let upper_eyelid_points_right_control = [];
let upper_eyelid_left_control_point = [P0_upper[0] * (1 - rands.left_converge0) + P1_lower[0] * rands.left_converge0, P0_upper[1] * (1 - rands.left_converge0) + P1_lower[1] * rands.left_converge0];
let upper_eyelid_right_control_point = [P3_upper[0] * (1 - rands.right_converge0) + P2_lower[0] * rands.right_converge0, P3_upper[1] * (1 - rands.right_converge0) + P2_lower[1] * rands.right_converge0];
for (let t = 0; t < 100; t++) {
upper_eyelid_points.push(cubicBezier(P0_upper, P1_upper, P2_upper, P3_upper, t / 100));
upper_eyelid_points_left_control.push(cubicBezier(upper_eyelid_left_control_point, P0_upper, P1_upper, P2_upper, t / 100));
upper_eyelid_points_right_control.push(cubicBezier(P1_upper, P2_upper, P3_upper, upper_eyelid_right_control_point, t / 100));
}
for (let i = 0; i < 75; i++) {
let weight = ((75.0 - i) / 75.0) ** 2
upper_eyelid_points[i] = [upper_eyelid_points[i][0] * (1 - weight) + upper_eyelid_points_left_control[i + 25][0] * weight, upper_eyelid_points[i][1] * (1 - weight) + upper_eyelid_points_left_control[i + 25][1] * weight]
upper_eyelid_points[i + 25] = [upper_eyelid_points[i + 25][0] * weight + upper_eyelid_points_right_control[i][0] * (1 - weight), upper_eyelid_points[i + 25][1] * weight + upper_eyelid_points_right_control[i][1] * (1 - weight)]
}
// now we generate the points for the upper eyelid
let lower_eyelid_points = [];
let lower_eyelid_points_left_control = [];
let lower_eyelid_points_right_control = [];
let lower_eyelid_left_control_point = [P0_lower[0] * (1 - rands.left_converge0) + P1_upper[0] * rands.left_converge0, P0_lower[1] * (1 - rands.left_converge0) + P1_upper[1] * rands.left_converge0];
let lower_eyelid_right_control_point = [P3_lower[0] * (1 - rands.right_converge1) + P2_upper[0] * rands.right_converge1, P3_lower[1] * (1 - rands.right_converge1) + P2_upper[1] * rands.right_converge1];
for (let t = 0; t < 100; t++) {
lower_eyelid_points.push(cubicBezier(P0_lower, P1_lower, P2_lower, P3_lower, t / 100));
lower_eyelid_points_left_control.push(cubicBezier(lower_eyelid_left_control_point, P0_lower, P1_lower, P2_lower, t / 100));
lower_eyelid_points_right_control.push(cubicBezier(P1_lower, P2_lower, P3_lower, lower_eyelid_right_control_point, t / 100));
}
for (let i = 0; i < 75; i++) {
let weight = ((75.0 - i) / 75.0) ** 2
lower_eyelid_points[i] = [lower_eyelid_points[i][0] * (1 - weight) + lower_eyelid_points_left_control[i + 25][0] * weight, lower_eyelid_points[i][1] * (1 - weight) + lower_eyelid_points_left_control[i + 25][1] * weight]
lower_eyelid_points[i + 25] = [lower_eyelid_points[i + 25][0] * weight + lower_eyelid_points_right_control[i][0] * (1 - weight), lower_eyelid_points[i + 25][1] * weight + lower_eyelid_points_right_control[i][1] * (1 - weight)]
}
for (let i = 0; i < 100; i++) {
lower_eyelid_points[i][1] = -lower_eyelid_points[i][1]
upper_eyelid_points[i][1] = -upper_eyelid_points[i][1]
}
let eyeCenter = [upper_eyelid_points[50][0] / 2.0 + lower_eyelid_points[50][0] / 2.0, upper_eyelid_points[50][1] / 2.0 + lower_eyelid_points[50][1] / 2.0];
for (let i = 0; i < 100; i++) {
// translate to center
lower_eyelid_points[i][0] -= eyeCenter[0]
lower_eyelid_points[i][1] -= eyeCenter[1]
upper_eyelid_points[i][0] -= eyeCenter[0]
upper_eyelid_points[i][1] -= eyeCenter[1]
}
eyeCenter = [0, 0];
// we switch the upper and lower eyelid points because in svg the bottom is y+ and top is y-
return { upper: upper_eyelid_points, lower: lower_eyelid_points, center: [eyeCenter]}
}
export function generateBothEyes(width = 50) {
let rands_left = generateEyeParameters(width)
// Create a shallow copy of the object
let rands_right = { ...rands_left };
// Iterate over the object's keys
for (let key in rands_right) {
// Check if the property value is a number
if (typeof rands_right[key] === 'number') {
// Add a random value to the number, for example, between -5 and 5
rands_right[key] += randomFromInterval(-rands_right[key] / 2.0, rands_right[key] / 2.0);
}
}
let left_eye = generateEyePoints(rands_left, width)
let right_eye = generateEyePoints(rands_right, width)
for (let key in left_eye) {
if (typeof left_eye[key] === 'object') {
for (let i = 0; i < left_eye[key].length; i++) {
left_eye[key][i][0] = -left_eye[key][i][0]
}
}
}
return { left: left_eye, right: right_eye }
}

View file

@ -0,0 +1,211 @@
function randomFromInterval(min, max) {
// min and max included
return Math.random() * (max - min) + min;
}
export function getEggShapePoints(a, b, k, segment_points) {
// the function is x^2/a^2 * (1 + ky) + y^2/b^2 = 1
var result = [];
// var pointString = "";
for (var i = 0; i < segment_points; i++) {
// x positive, y positive
// first compute the degree
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points,
);
var y = Math.sin(degree) * b;
var x =
Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
for (var i = segment_points; i > 0; i--) {
// x is negative, y is positive
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points,
);
var y = Math.sin(degree) * b;
var x =
-Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
for (var i = 0; i < segment_points; i++) {
// x is negative, y is negative
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points,
);
var y = -Math.sin(degree) * b;
var x =
-Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
for (var i = segment_points; i > 0; i--) {
// x is positive, y is negative
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points,
);
var y = -Math.sin(degree) * b;
var x =
Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
return result;
}
function findIntersectionPoints(radian, a, b) {
if (radian < 0) {
radian = 0;
}
if (radian > Math.PI / 2) {
radian = Math.PI / 2;
}
// a is width, b is height
// Slope of the line
const m = Math.tan(radian);
// check if radian is close to 90 degrees
if (Math.abs(radian - Math.PI / 2) < 0.0001) {
return { x: 0, y: b };
}
// only checks the first quadrant
const y = m * a;
if (y < b) {
// it intersects with the left side
return { x: a, y: y };
} else {
// it intersects with the top side
// console.log(m);
const x = b / m;
// console.log(x, b);
return { x: x, y: b };
}
}
export function generateRectangularFaceContourPoints(a, b, segment_points) {
// a is width, b is height, segment_points is the number of points
var result = [];
for (var i = 0; i < segment_points; i++) {
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 11 / segment_points,
Math.PI / 11 / segment_points,
);
var intersection = findIntersectionPoints(degree, a, b);
result.push([intersection.x, intersection.y]);
}
for (var i = segment_points; i > 0; i--) {
// x is negative, y is positive
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 11 / segment_points,
Math.PI / 11 / segment_points,
);
var intersection = findIntersectionPoints(degree, a, b);
result.push([-intersection.x, intersection.y]);
}
for (var i = 0; i < segment_points; i++) {
// x is negative, y is negative
// first compute the degree
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 11 / segment_points,
Math.PI / 11 / segment_points,
);
var intersection = findIntersectionPoints(degree, a, b);
result.push([-intersection.x, -intersection.y]);
}
for (var i = segment_points; i > 0; i--) {
// x is positive, y is negative
// first compute the degree
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 11 / segment_points,
Math.PI / 11 / segment_points,
);
var intersection = findIntersectionPoints(degree, a, b);
result.push([intersection.x, -intersection.y]);
}
return result;
}
export function generateFaceCountourPoints(numPoints = 100) {
var faceSizeX0 = randomFromInterval(50, 100);
var faceSizeY0 = randomFromInterval(70, 100);
var faceSizeY1 = randomFromInterval(50, 80);
var faceSizeX1 = randomFromInterval(70, 100);
var faceK0 =
randomFromInterval(0.001, 0.005) * (Math.random() > 0.5 ? 1 : -1);
var faceK1 =
randomFromInterval(0.001, 0.005) * (Math.random() > 0.5 ? 1 : -1);
var face0TranslateX = randomFromInterval(-5, 5);
var face0TranslateY = randomFromInterval(-15, 15);
var face1TranslateY = randomFromInterval(-5, 5);
var face1TranslateX = randomFromInterval(-5, 25);
var eggOrRect0 = Math.random() > 0.1;
var eggOrRect1 = Math.random() > 0.3;
var results0 = eggOrRect0
? getEggShapePoints(faceSizeX0, faceSizeY0, faceK0, numPoints)
: generateRectangularFaceContourPoints(faceSizeX0, faceSizeY0, numPoints);
var results1 = eggOrRect1
? getEggShapePoints(faceSizeX1, faceSizeY1, faceK1, numPoints)
: generateRectangularFaceContourPoints(faceSizeX1, faceSizeY1, numPoints);
for (var i = 0; i < results0.length; i++) {
results0[i][0] += face0TranslateX;
results0[i][1] += face0TranslateY;
results1[i][0] += face1TranslateX;
results1[i][1] += face1TranslateY;
}
var results = [];
let center = [0, 0];
for (var i = 0; i < results0.length; i++) {
results.push([
results0[i][0] * 0.7 +
results1[(i + results0.length / 4) % results0.length][1] * 0.3,
results0[i][1] * 0.7 -
results1[(i + results0.length / 4) % results0.length][0] * 0.3,
]);
center[0] += results[i][0];
center[1] += results[i][1];
}
center[0] /= results.length;
center[1] /= results.length;
// center the face
for (var i = 0; i < results.length; i++) {
results[i][0] -= center[0];
results[i][1] -= center[1];
}
let width = results[0][0] - results[results.length / 2][0];
let height =
results[results.length / 4][1] - results[(results.length * 3) / 4][1];
// add the first point to the end to close the shape
results.push(results[0]);
results.push(results[1]);
// console.log(results);
return { face: results, width: width, height: height, center: [0, 0] };
}

View file

@ -0,0 +1,152 @@
function randomFromInterval(min, max) {
// min and max included
return Math.random() * (max - min) + min;
}
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
function binomialCoefficient(n, k) {
return factorial(n) / (factorial(k) * factorial(n - k));
}
function calculateBezierPoint(t, controlPoints) {
let x = 0, y = 0;
const n = controlPoints.length - 1;
for (let i = 0; i <= n; i++) {
let binCoeff = binomialCoefficient(n, i);
let a = Math.pow(1 - t, n - i);
let b = Math.pow(t, i);
x += binCoeff * a * b * controlPoints[i].x;
y += binCoeff * a * b * controlPoints[i].y;
}
return [x, y];
}
function computeBezierCurve(controlPoints, numberOfPoints) {
let curve = [];
for (let i = 0; i <= numberOfPoints; i++) {
let t = i / numberOfPoints;
let point = calculateBezierPoint(t, controlPoints);
curve.push(point);
}
return curve;
}
export function generateHairLines0(faceCountour, numHairLines = 100) {
var faceCountourCopy = faceCountour.slice(0, faceCountour.length - 2);
var results = [];
for (var i = 0; i < numHairLines; i++){
var numHairPoints = 20 + Math.floor(randomFromInterval(-5, 5));
// we generate some hair lines
var hair_line = [];
var index_offset = Math.floor(randomFromInterval(30, 140));
for (var j = 0; j < numHairPoints; j++){
hair_line.push({x: faceCountourCopy[(faceCountourCopy.length - (j + index_offset)) % faceCountourCopy.length][0], y:faceCountourCopy[(faceCountourCopy.length - (j + index_offset)) % faceCountourCopy.length][1]});
}
var d0 = computeBezierCurve(hair_line, numHairPoints);
hair_line = []
index_offset = Math.floor(randomFromInterval(30, 140));
for (var j = 0; j < numHairPoints; j++){
hair_line.push({x: faceCountourCopy[(faceCountourCopy.length - (-j + index_offset)) % faceCountourCopy.length][0], y:faceCountourCopy[(faceCountourCopy.length - (-j + index_offset)) % faceCountourCopy.length][1]});
}
var d1 = computeBezierCurve(hair_line, numHairPoints);
var d = [];
for (var j = 0; j < numHairPoints; j++){
d.push([d0[j][0] * (j * (1 / numHairPoints)) ** 2 + d1[j][0] * (1 - (j * (1 / numHairPoints)) ** 2), d0[j][1] * (j * (1 / numHairPoints)) ** 2 + d1[j][1] * (1 - (j * (1 / numHairPoints)) ** 2)]);
}
results.push(d);
}
return results;
}
export function generateHairLines1(faceCountour, numHairLines = 100) {
var faceCountourCopy = faceCountour.slice(0, faceCountour.length - 2);
var results = [];
for (var i = 0; i < numHairLines; i++){
var numHairPoints = 20 + Math.floor(randomFromInterval(-5, 5));
// we generate some hair lines
var hair_line = [];
var index_start = Math.floor(randomFromInterval(20, 160));
hair_line.push({x: faceCountourCopy[(faceCountourCopy.length - index_start) % faceCountourCopy.length][0], y:faceCountourCopy[(faceCountourCopy.length - index_start) % faceCountourCopy.length][1]});
for (var j = 1; j < numHairPoints + 1; j++){
index_start = Math.floor(randomFromInterval(20, 160));
hair_line.push({x: faceCountourCopy[(faceCountourCopy.length - index_start) % faceCountourCopy.length][0], y:faceCountourCopy[(faceCountourCopy.length - index_start) % faceCountourCopy.length][1]});
}
var d = computeBezierCurve(hair_line, numHairPoints);
results.push(d);
}
return results;
}
export function generateHairLines2(faceCountour, numHairLines = 100) {
var faceCountourCopy = faceCountour.slice(0, faceCountour.length - 2);
var results = [];
var pickedIndices = [];
for (var i = 0; i < numHairLines; i++){
pickedIndices.push(Math.floor(randomFromInterval(10, 180)));
}
pickedIndices.sort();
for (var i = 0; i < numHairLines; i++){
var numHairPoints = 20 + Math.floor(randomFromInterval(-5, 5));
// we generate some hair lines
var hair_line = [];
var index_offset = pickedIndices[i];
var lower = randomFromInterval(0.8 , 1.4);
var reverse = Math.random() > 0.5 ? 1 : -1;
for (var j = 0; j < numHairPoints; j++){
var powerscale = randomFromInterval(0.1, 3);
var portion = (1 - (j / numHairPoints) ** powerscale) * (1 - lower) + lower;
hair_line.push({x: faceCountourCopy[(faceCountourCopy.length - (reverse * j + index_offset)) % faceCountourCopy.length][0] * portion, y:faceCountourCopy[(faceCountourCopy.length - (reverse * j + index_offset)) % faceCountourCopy.length][1] * portion});
}
var d = computeBezierCurve(hair_line, numHairPoints);
if (Math.random() > 0.7) d = d.reverse();
if (results.length == 0){
results.push(d);
continue;
}
var lastHairPoint = results[results.length - 1][results[results.length - 1].length - 1];
var lastPointsDistance = Math.sqrt((d[0][0] - lastHairPoint[0]) ** 2 + (d[0][1] - lastHairPoint[1]) ** 2);
if (Math.random() > 0.5 && lastPointsDistance < 100){
results[results.length - 1] = results[results.length - 1].concat(d);
}else{
results.push(d);
}
}
return results;
}
export function generateHairLines3(faceCountour, numHairLines = 100) {
var faceCountourCopy = faceCountour.slice(0, faceCountour.length - 2);
var results = [];
var pickedIndices = [];
for (var i = 0; i < numHairLines; i++){
pickedIndices.push(Math.floor(randomFromInterval(10, 180)));
}
pickedIndices.sort();
var splitPoint = Math.floor(randomFromInterval(0, 200));
for (var i = 0; i < numHairLines; i++){
var numHairPoints = 30 + Math.floor(randomFromInterval(-8, 8));
// we generate some hair lines
var hair_line = [];
var index_offset = pickedIndices[i];
var lower = randomFromInterval(1 , 2.3);
if (Math.random() > 0.9) lower = randomFromInterval(0 , 1.);
var reverse = index_offset > splitPoint ? 1 : -1;
for (var j = 0; j < numHairPoints; j++){
var powerscale = randomFromInterval(0.1, 3);
var portion = (1 - (j / (numHairPoints)) ** powerscale) * (1 - lower) + lower;
hair_line.push({x: faceCountourCopy[(faceCountourCopy.length - (reverse * j * 2 + index_offset)) % faceCountourCopy.length][0] * portion, y:faceCountourCopy[(faceCountourCopy.length - (reverse * j * 2 + index_offset)) % faceCountourCopy.length][1]});
}
var d = computeBezierCurve(hair_line, numHairPoints);
results.push(d);
}
return results;
}

260
src/lib/uglyAvatar/index.js Normal file
View file

@ -0,0 +1,260 @@
/**
* Ported from the ugly-avatar project
* Repohttps://github.com/txstc55/ugly-avatar
*/
import { generateFaceCountourPoints } from './face_shape.js'
import { generateBothEyes } from './eye_shape.js'
import { generateHairLines0, generateHairLines1, generateHairLines2, generateHairLines3 } from './hair_lines.js'
import { generateMouthShape0, generateMouthShape1, generateMouthShape2 } from './mouth_shape.js'
// createElement function
const createElement = (template) => {
return new Range().createContextualFragment(template).firstElementChild
}
function randomFromInterval(min, max) {
return Math.random() * (max - min) + min
}
function createAvatarSvg() {
const data = {
faceScale: 1.8, // face scale
computedFacePoints: [], // the polygon points for face countour
eyeRightUpper: [], // the points for right eye upper lid
eyeRightLower: [],
eyeRightCountour: [], // for the white part of the eye
eyeLeftUpper: [],
eyeLeftLower: [],
eyeLeftCountour: [],
faceHeight: 0, // the height of the face
faceWidth: 0, // the width of the face
center: [0, 0], // the center of the face
distanceBetweenEyes: 0, // the distance between the eyes
leftEyeOffsetX: 0, // the offset of the left eye
leftEyeOffsetY: 0, // the offset of the left eye
rightEyeOffsetX: 0, // the offset of the right eye
rightEyeOffsetY: 0, // the offset of the right eye
eyeHeightOffset: 0, // the offset of the eye height
leftEyeCenter: [0, 0], // the center of the left eye
rightEyeCenter: [0, 0], // the center of the right eye
rightPupilShiftX: 0, // the shift of the right pupil
rightPupilShiftY: 0, // the shift of the right pupil
leftPupilShiftX: 0, // the shift of the left pupil
leftPupilShiftY: 0, // the shift of the left pupil
rightNoseCenterX: 0, // the center of the right nose
rightNoseCenterY: 0, // the center of the right nose
leftNoseCenterX: 0, // the center of the left nose
leftNoseCenterY: 0, // the center of the left nose
hairs: [],
haventSleptForDays: false,
hairColors: [
'rgb(0, 0, 0)', // Black
'rgb(44, 34, 43)', // Dark Brown
'rgb(80, 68, 68)', // Medium Brown
'rgb(167, 133, 106)', // Light Brown
'rgb(220, 208, 186)', // Blond
'rgb(233, 236, 239)', // Platinum Blond
'rgb(165, 42, 42)', // Red
'rgb(145, 85, 61)', // Auburn
'rgb(128, 128, 128)', // Grey
'rgb(185, 55, 55)' // Fire
// ... 其他颜色
],
hairColor: 'black',
dyeColorOffset: '50%',
backgroundColors: [
'rgb(245, 245, 220)', // Soft Beige
'rgb(176, 224, 230)', // Pale Blue
'rgb(211, 211, 211)', // Light Grey
'rgb(152, 251, 152)', // Pastel Green
'rgb(255, 253, 208)', // Cream
'rgb(230, 230, 250)', // Muted Lavender
'rgb(188, 143, 143)', // Dusty Rose
'rgb(135, 206, 235)', // Sky Blue
'rgb(245, 255, 250)', // Mint Cream
'rgb(245, 222, 179)' // Wheat
// ... 其他颜色
],
mouthPoints: []
}
data.faceScale = 1.5 + Math.random() * 0.6
data.haventSleptForDays = Math.random() > 0.8
let faceResults = generateFaceCountourPoints()
data.computedFacePoints = faceResults.face
data.faceHeight = faceResults.height
data.faceWidth = faceResults.width
data.center = faceResults.center
let eyes = generateBothEyes(data.faceWidth / 2)
let left = eyes.left
let right = eyes.right
data.eyeRightUpper = right.upper
data.eyeRightLower = right.lower
data.eyeRightCountour = right.upper.slice(10, 90).concat(right.lower.slice(10, 90).reverse())
data.eyeLeftUpper = left.upper
data.eyeLeftLower = left.lower
data.eyeLeftCountour = left.upper.slice(10, 90).concat(left.lower.slice(10, 90).reverse())
data.distanceBetweenEyes = randomFromInterval(data.faceWidth / 4.5, data.faceWidth / 4)
data.eyeHeightOffset = randomFromInterval(data.faceHeight / 8, data.faceHeight / 6)
data.leftEyeOffsetX = randomFromInterval(-data.faceWidth / 20, data.faceWidth / 10)
data.leftEyeOffsetY = randomFromInterval(-data.faceHeight / 50, data.faceHeight / 50)
data.rightEyeOffsetX = randomFromInterval(-data.faceWidth / 20, data.faceWidth / 10)
data.rightEyeOffsetY = randomFromInterval(-data.faceHeight / 50, data.faceHeight / 50)
data.leftEyeCenter = left.center[0]
data.rightEyeCenter = right.center[0]
data.leftPupilShiftX = randomFromInterval(-data.faceWidth / 20, data.faceWidth / 20)
// now we generate the pupil shifts
// we first pick a point from the upper eye lid
let leftInd0 = Math.floor(randomFromInterval(10, left.upper.length - 10))
let rightInd0 = Math.floor(randomFromInterval(10, right.upper.length - 10))
let leftInd1 = Math.floor(randomFromInterval(10, left.upper.length - 10))
let rightInd1 = Math.floor(randomFromInterval(10, right.upper.length - 10))
let leftLerp = randomFromInterval(0.2, 0.8)
let rightLerp = randomFromInterval(0.2, 0.8)
data.leftPupilShiftY = left.upper[leftInd0][1] * leftLerp + left.lower[leftInd1][1] * (1 - leftLerp)
data.rightPupilShiftY = right.upper[rightInd0][1] * rightLerp + right.lower[rightInd1][1] * (1 - rightLerp)
data.leftPupilShiftX = left.upper[leftInd0][0] * leftLerp + left.lower[leftInd1][0] * (1 - leftLerp)
data.rightPupilShiftX = right.upper[rightInd0][0] * rightLerp + right.lower[rightInd1][0] * (1 - rightLerp)
var numHairLines = []
var numHairMethods = 4
for (var i = 0; i < numHairMethods; i++) {
numHairLines.push(Math.floor(randomFromInterval(0, 50)))
}
data.hairs = []
if (Math.random() > 0.3) {
data.hairs = generateHairLines0(data.computedFacePoints, numHairLines[0] * 1 + 10)
}
if (Math.random() > 0.3) {
data.hairs = data.hairs.concat(generateHairLines1(data.computedFacePoints, numHairLines[1] / 1.5 + 10))
}
if (Math.random() > 0.5) {
data.hairs = data.hairs.concat(generateHairLines2(data.computedFacePoints, numHairLines[2] * 3 + 10))
}
if (Math.random() > 0.5) {
data.hairs = data.hairs.concat(generateHairLines3(data.computedFacePoints, numHairLines[3] * 3 + 10))
}
data.rightNoseCenterX = randomFromInterval(data.faceWidth / 18, data.faceWidth / 12)
data.rightNoseCenterY = randomFromInterval(0, data.faceHeight / 5)
data.leftNoseCenterX = randomFromInterval(-data.faceWidth / 18, -data.faceWidth / 12)
data.leftNoseCenterY = data.rightNoseCenterY + randomFromInterval(-data.faceHeight / 30, data.faceHeight / 20)
if (Math.random() > 0.1) {
// use natural hair color
data.hairColor = data.hairColors[Math.floor(Math.random() * 10)]
} else {
data.hairColor = 'url(#rainbowGradient)'
data.dyeColorOffset = randomFromInterval(0, 100) + '%'
}
var choice = Math.floor(Math.random() * 3)
if (choice == 0) {
data.mouthPoints = generateMouthShape0(data.computedFacePoints, data.faceHeight, data.faceWidth)
} else if (choice == 1) {
data.mouthPoints = generateMouthShape1(data.computedFacePoints, data.faceHeight, data.faceWidth)
} else {
data.mouthPoints = generateMouthShape2(data.computedFacePoints, data.faceHeight, data.faceWidth)
}
const svgTemplate = `
<svg viewBox="-100 -100 200 200" xmlns="http://www.w3.org/2000/svg" width="500" height="500" id="face-svg">
<defs>
<clipPath id="leftEyeClipPath">
<polyline points="${data.eyeLeftUpper.concat(data.eyeLeftLower.reverse()).join(' ')}" />
</clipPath>
<clipPath id="rightEyeClipPath">
<polyline points="${data.eyeRightUpper.concat(data.eyeRightLower.reverse()).join(' ')}" />
</clipPath>
<filter id="fuzzy">
<feTurbulence id="turbulence" baseFrequency="0.05" numOctaves="3" type="turbulence" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="2" />
</filter>
<linearGradient id="rainbowGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color: ${data.hairColors[Math.floor(Math.random() * data.hairColors.length)]}; stop-opacity: 1" />
<stop offset="${data.dyeColorOffset}" style="stop-color: ${data.hairColors[Math.floor(Math.random() * data.hairColors.length)]}; stop-opacity: 1" />
<stop offset="100%" style="stop-color: ${data.hairColors[Math.floor(Math.random() * data.hairColors.length)]}; stop-opacity: 1" />
</linearGradient>
</defs>
<rect x="-100" y="-100" width="100%" height="100%" fill="${data.backgroundColors[Math.floor(Math.random() * data.backgroundColors.length)]}" />
<polyline id="faceContour" points="${data.computedFacePoints.join(' ')}" fill="#ffc9a9" stroke="black" stroke-width="${3.0 / data.faceScale}" stroke-linejoin="round" filter="url(#fuzzy)" />
<g transform="translate(${data.center[0] + data.distanceBetweenEyes + data.rightEyeOffsetX} ${-(-data.center[1] + data.eyeHeightOffset + data.rightEyeOffsetY)})">
<polyline id="rightCountour" points="${data.eyeRightUpper.concat(data.eyeRightLower.reverse()).join(' ')}" fill="white" stroke="white" stroke-width="${0.0 / data.faceScale}" stroke-linejoin="round" filter="url(#fuzzy)" />
</g>
<g transform="translate(${-(data.center[0] + data.distanceBetweenEyes + data.leftEyeOffsetX)} ${-(-data.center[1] + data.eyeHeightOffset + data.leftEyeOffsetY)})">
<polyline id="leftCountour" points="${data.eyeLeftUpper.concat(data.eyeLeftLower.reverse()).join(' ')}" fill="white" stroke="white" stroke-width="${0.0 / data.faceScale}" stroke-linejoin="round" filter="url(#fuzzy)" />
</g>
<g transform="translate(${data.center[0] + data.distanceBetweenEyes + data.rightEyeOffsetX} ${-(-data.center[1] + data.eyeHeightOffset + data.rightEyeOffsetY)})">
<polyline id="rightUpper" points="${data.eyeRightUpper.join(' ')}" fill="none" stroke="black" stroke-width="${(data.haventSleptForDays ? 5.0 : 3.0) / data.faceScale}" stroke-linejoin="round" stroke-linecap="round" filter="url(#fuzzy)" />
<polyline id="rightLower" points="${data.eyeRightLower.join(' ')}" fill="none" stroke="black" stroke-width="${(data.haventSleptForDays ? 5.0 : 3.0) / data.faceScale}" stroke-linejoin="round" stroke-linecap="round" filter="url(#fuzzy)" />
${Array.from(
{ length: 10 },
(_, i) => `
<circle r="${Math.random() * 2 + 3.0}" cx="${data.rightPupilShiftX + Math.random() * 5 - 2.5}" cy="${data.rightPupilShiftY + Math.random() * 5 - 2.5}" stroke="black" fill="none" stroke-width="${1.0 + Math.random() * 0.5}" filter="url(#fuzzy)" clip-path="url(#rightEyeClipPath)" />
`
).join('')}
</g>
<g transform="translate(${-(data.center[0] + data.distanceBetweenEyes + data.leftEyeOffsetX)} ${-(-data.center[1] + data.eyeHeightOffset + data.leftEyeOffsetY)})">
<polyline id="leftUpper" points="${data.eyeLeftUpper.join(' ')}" fill="none" stroke="black" stroke-width="${(data.haventSleptForDays ? 5.0 : 3.0) / data.faceScale}" stroke-linejoin="round" filter="url(#fuzzy)" />
<polyline id="leftLower" points="${data.eyeLeftLower.join(' ')}" fill="none" stroke="black" stroke-width="${(data.haventSleptForDays ? 5.0 : 3.0) / data.faceScale}" stroke-linejoin="round" filter="url(#fuzzy)" />
${Array.from(
{ length: 10 },
(_, i) => `
<circle r="${Math.random() * 2 + 3.0}" cx="${data.leftPupilShiftX + Math.random() * 5 - 2.5}" cy="${data.leftPupilShiftY + Math.random() * 5 - 2.5}" stroke="black" fill="none" stroke-width="${1.0 + Math.random() * 0.5}" filter="url(#fuzzy)" clip-path="url(#leftEyeClipPath)" />
`
).join('')}
</g>
<g id="hairs">
${data.hairs
.map(
(hair, index) => `
<polyline points="${hair.join(' ')}" fill="none" stroke="${data.hairColor}" stroke-width="${0.5 + Math.random() * 2.5}" stroke-linejoin="round" filter="url(#fuzzy)" />
`
)
.join('')}
</g>
${
Math.random() > 0.5
? `
<g id="pointNose">
<g id="rightNose">
${Array.from(
{ length: 10 },
(_, i) => `
<circle r="${Math.random() * 2 + 1.0}" cx="${data.rightNoseCenterX + Math.random() * 4 - 2}" cy="${data.rightNoseCenterY + Math.random() * 4 - 2}" stroke="black" fill="none" stroke-width="${1.0 + Math.random() * 0.5}" filter="url(#fuzzy)" />
`
).join('')}
</g>
<g id="leftNose">
${Array.from(
{ length: 10 },
(_, i) => `
<circle r="${Math.random() * 2 + 1.0}" cx="${data.leftNoseCenterX + Math.random() * 4 - 2}" cy="${data.leftNoseCenterY + Math.random() * 4 - 2}" stroke="black" fill="none" stroke-width="${1.0 + Math.random() * 0.5}" filter="url(#fuzzy)" />
`
).join('')}
</g>
</g>
`
: `
<g id="lineNose">
<path d="M ${data.leftNoseCenterX} ${data.leftNoseCenterY}, Q${data.rightNoseCenterX} ${data.rightNoseCenterY * 1.5},${(data.leftNoseCenterX + data.rightNoseCenterX) / 2} ${-data.eyeHeightOffset * 0.2}" fill="none" stroke="black" stroke-width="${2.5 + Math.random() * 1.0}" stroke-linejoin="round" filter="url(#fuzzy)"></path>
</g>
`
}
<g id="mouth">
<polyline points="${data.mouthPoints.join(' ')}" fill="rgb(215,127,140)" stroke="black" stroke-width="${2.7 + Math.random() * 0.5}" stroke-linejoin="round" filter="url(#fuzzy)" />
</g>
</svg>
`
return createElement(svgTemplate)
}
// 导出函数
export default function generateUglyAvatar() {
const svgElement = createAvatarSvg()
const serializer = new XMLSerializer()
const svgString = serializer.serializeToString(svgElement)
const svgBlob = new Blob([svgString], { type: 'image/svg+xml' })
return svgBlob
}

View file

@ -0,0 +1,171 @@
function randomFromInterval(min, max) {
// min and max included
return Math.random() * (max - min) + min;
}
function cubicBezier(P0, P1, P2, P3, t) {
var x = (1 - t) ** 3 * P0[0] + 3 * (1 - t) ** 2 * t * P1[0] + 3 * (1 - t) * t ** 2 * P2[0] + t ** 3 * P3[0];
var y = (1 - t) ** 3 * P0[1] + 3 * (1 - t) ** 2 * t * P1[1] + 3 * (1 - t) * t ** 2 * P2[1] + t ** 3 * P3[1];
return [x, y];
}
function getEggShapePoints(a, b, k, segment_points) {
// the function is x^2/a^2 * (1 + ky) + y^2/b^2 = 1
var result = [];
// var pointString = "";
for (var i = 0; i < segment_points; i++) {
// x positive, y positive
// first compute the degree
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points
);
var y = Math.sin(degree) * b;
var x =
Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
for (var i = segment_points; i > 0; i--) {
// x is negative, y is positive
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points
);
var y = Math.sin(degree) * b;
var x =
-Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
for (var i = 0; i < segment_points; i++) {
// x is negative, y is negative
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points
);
var y = -Math.sin(degree) * b;
var x =
-Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
for (var i = segment_points; i > 0; i--) {
// x is positive, y is negative
var degree =
(Math.PI / 2 / segment_points) * i +
randomFromInterval(
-Math.PI / 1.1 / segment_points,
Math.PI / 1.1 / segment_points
);
var y = -Math.sin(degree) * b;
var x =
Math.sqrt(((1 - (y * y) / (b * b)) / (1 + k * y)) * a * a) +
randomFromInterval(-a / 200.0, a / 200.0);
// pointString += x + "," + y + " ";
result.push([x, y]);
}
return result;
}
export function generateMouthShape0(faceCountour, faceHeight, faceWidth) {
// the first one is a a big smile U shape
var faceCountourCopy = faceCountour.slice(0, faceCountour.length - 2);
// choose one point on face at bottom side
var mouthRightY = randomFromInterval(faceHeight / 7, faceHeight / 3.5)
var mouthLeftY = randomFromInterval(faceHeight / 7, faceHeight / 3.5)
var mouthRightX = randomFromInterval(faceWidth / 10, faceWidth / 2)
var mouthLeftX = -mouthRightX + randomFromInterval(-faceWidth / 20, faceWidth / 20)
var mouthRight = [mouthRightX, mouthRightY]
var mouthLeft = [mouthLeftX, mouthLeftY]
var controlPoint0 = [randomFromInterval(0, mouthRightX), randomFromInterval(mouthLeftY + 5, faceHeight / 1.5)]
var controlPoint1 = [randomFromInterval(mouthLeftX, 0), randomFromInterval(mouthLeftY + 5, faceHeight / 1.5)]
var mouthPoints = []
for (var i = 0; i < 1; i += 0.01) {
mouthPoints.push(cubicBezier(mouthLeft, controlPoint1, controlPoint0, mouthRight, i))
}
if (Math.random() > 0.5) {
for (var i = 0; i < 1; i += 0.01) {
mouthPoints.push(cubicBezier(mouthRight, controlPoint0, controlPoint1, mouthLeft, i))
}
}else{
var y_offset_portion = randomFromInterval(0, 0.8);
for (var i = 0; i < 100; i += 1) {
mouthPoints.push([mouthPoints[99][0] * (1 - i / 100.0) + mouthPoints[0][0] * i / 100.0, (mouthPoints[99][1] * (1 - i / 100.0) + mouthPoints[0][1] * i / 100.0) * (1 - y_offset_portion) + mouthPoints[99 - i][1] * y_offset_portion])
}
}
return mouthPoints;
}
export function generateMouthShape1(faceCountour, faceHeight, faceWidth) {
// the first one is a a big smile U shape
var faceCountourCopy = faceCountour.slice(0, faceCountour.length - 2);
// choose one point on face at bottom side
var mouthRightY = randomFromInterval(faceHeight / 7, faceHeight / 4)
var mouthLeftY = randomFromInterval(faceHeight / 7, faceHeight / 4)
var mouthRightX = randomFromInterval(faceWidth / 10, faceWidth / 2)
var mouthLeftX = -mouthRightX + randomFromInterval(-faceWidth / 20, faceWidth / 20)
var mouthRight = [mouthRightX, mouthRightY]
var mouthLeft = [mouthLeftX, mouthLeftY]
var controlPoint0 = [randomFromInterval(0, mouthRightX), randomFromInterval(mouthLeftY + 5, faceHeight / 1.5)]
var controlPoint1 = [randomFromInterval(mouthLeftX, 0), randomFromInterval(mouthLeftY + 5, faceHeight / 1.5)]
var mouthPoints = []
for (var i = 0; i < 1; i += 0.01) {
mouthPoints.push(cubicBezier(mouthLeft, controlPoint1, controlPoint0, mouthRight, i))
}
var center = [(mouthRight[0] + mouthLeft[0]) / 2, mouthPoints[25][1] / 2 + mouthPoints[75][1] / 2];
if (Math.random() > 0.5) {
for (var i = 0; i < 1; i += 0.01) {
mouthPoints.push(cubicBezier(mouthRight, controlPoint0, controlPoint1, mouthLeft, i))
}
}else{
var y_offset_portion = randomFromInterval(0, 0.8);
for (var i = 0; i < 100; i += 1) {
mouthPoints.push([mouthPoints[99][0] * (1 - i / 100.0) + mouthPoints[0][0] * i / 100.0, (mouthPoints[99][1] * (1 - i / 100.0) + mouthPoints[0][1] * i / 100.0) * (1 - y_offset_portion) + mouthPoints[99 - i][1] * y_offset_portion])
}
}
// translate to center
for (var i = 0; i < mouthPoints.length; i++) {
mouthPoints[i][0] -= center[0]
mouthPoints[i][1] -= center[1]
// rotate 180 degree
mouthPoints[i][1] = -mouthPoints[i][1]
// scale smaller
mouthPoints[i][0] = mouthPoints[i][0] * 0.6
mouthPoints[i][1] = mouthPoints[i][1] * 0.6
// translate back
mouthPoints[i][0] += center[0]
mouthPoints[i][1] += center[1] * 0.8
}
return mouthPoints;
}
export function generateMouthShape2(faceCountour, faceHeight, faceWidth) {
// generate a random center
var center = [randomFromInterval(-faceWidth / 8, faceWidth / 8), randomFromInterval(faceHeight / 4, faceHeight / 2.5)]
var mouthPoints = getEggShapePoints(randomFromInterval(faceWidth / 4, faceWidth / 10), randomFromInterval(faceHeight / 10, faceHeight / 20), 0.001, 50);
var randomRotationDegree = randomFromInterval(-Math.PI / 9.5, Math.PI / 9.5)
for (var i = 0; i < mouthPoints.length; i++) {
// rotate the point
var x = mouthPoints[i][0]
var y = mouthPoints[i][1]
mouthPoints[i][0] = x * Math.cos(randomRotationDegree) - y * Math.sin(randomRotationDegree)
mouthPoints[i][1] = x * Math.sin(randomRotationDegree) + y * Math.cos(randomRotationDegree)
mouthPoints[i][0] += center[0]
mouthPoints[i][1] += center[1]
}
return mouthPoints;
}

View file

@ -1,12 +1,12 @@
export const chunk = <T = any>(array: T[], size: number) =>
Array.from({ length: Math.ceil(array.length / size) }, (_v, i) => array.slice(i * size, i * size + size))
export const desert = <T extends object>(target: T[], key: keyof T, value: T) => {
const index = target.findIndex((item) => item[key] === value[key])
export const desert = <T = any>(target: T[], value: T, key?: keyof T) => {
const index = target.findIndex((item) => (key ? item[key] === value[key] : value === item))
return index === -1 ? [...target, value] : target.toSpliced(index, 1)
}
export const upsert = <T extends object>(target: T[], key: keyof T, value: T) => {
const index = target.findIndex((item) => item[key] === value[key])
export const upsert = <T = any>(target: T[], value: T, key?: keyof T) => {
const index = target.findIndex((item) => (key ? item[key] === value[key] : value === item))
return index === -1 ? [...target, value] : target.toSpliced(index, 1, value)
}

View file

@ -1,111 +1,60 @@
// const compress = async (
// imageBitmap: ImageBitmap,
// targetSize: number,
// low: number,
// high: number,
// bestBlob: Blob
// ): Promise<Blob> => {
// // Calculate the middle value of quality
// const mid = (low + high) / 2
// // Calculate the width and height after scaling
// const width = imageBitmap.width * mid
// const height = imageBitmap.height * mid
// const offscreenCanvas = new OffscreenCanvas(width, height)
// const offscreenContext = offscreenCanvas.getContext('2d')!
// offscreenContext.drawImage(imageBitmap, 0, 0, width, height)
// const outputBlob = await offscreenCanvas.convertToBlob({ type: 'image/jpeg', quality: mid })
// // Calculate the current size based on the current quality
// const currentSize = outputBlob.size
// // If the current size is close to the target size, update the bestBlob
// if (currentSize <= targetSize && Math.abs(currentSize - targetSize) < Math.abs(bestBlob.size - targetSize)) {
// bestBlob = outputBlob
// }
// // If the current size is between -1024 ~ 0, return the result
// if ((currentSize - targetSize <= 0 && currentSize - targetSize >= -1024) || high - low < 0.01) {
// return bestBlob
// }
// // Adjust the range for recursion based on the current quality and size
// if (currentSize > targetSize) {
// return await compress(imageBitmap, targetSize, low, mid, bestBlob)
// } else {
// return await compress(imageBitmap, targetSize, mid, high, bestBlob)
// }
// }
// const compressImage = async (inputBlob: Blob, targetSize: number) => {
// // If the original size already meets the target size, return the original Blob
// if (inputBlob.size <= targetSize) {
// return inputBlob
// }
// // Initialize the range of quality
// const low = 0
// const high = 1
// // Initialize bestBlob with the original input Blob
// const bestBlob = inputBlob
// const imageBitmap = await createImageBitmap(inputBlob)
// // Call the recursive function
// return await compress(imageBitmap, targetSize, low, high, bestBlob)
// }
// export default compressImage
const compress = async (
imageBitmap: ImageBitmap,
targetSize: number,
errorMargin: number,
low: number,
high: number,
bestBlob: Blob
bestBlob: Blob,
threshold: number
): Promise<Blob> => {
// Calculate the middle value of quality
const mid = (low + high) / 2
// Calculate the width and height after scaling
const width = imageBitmap.width * mid
const height = imageBitmap.height * mid
const offscreenCanvas = new OffscreenCanvas(width, height)
const offscreenContext = offscreenCanvas.getContext('2d')!
offscreenContext.drawImage(imageBitmap, 0, 0, width, height)
const outputBlob = await offscreenCanvas.convertToBlob({ type: 'image/jpeg', quality: mid })
// Calculate the current size based on the current quality
const currentSize = outputBlob.size
if (Math.abs(currentSize - targetSize) < Math.abs(bestBlob.size - targetSize)) {
// If the current size is close to the target size, update the bestBlob
if (currentSize <= targetSize && Math.abs(currentSize - targetSize) < Math.abs(bestBlob.size - targetSize)) {
bestBlob = outputBlob
}
if (Math.abs(currentSize - targetSize) <= errorMargin || high - low < 0.01) {
// If the current size is between -1024 ~ 0, return the result
if ((currentSize - targetSize <= 0 && currentSize - targetSize >= -threshold) || high - low < 0.01) {
return bestBlob
}
// Adjust the range for recursion based on the current quality and size
if (currentSize > targetSize) {
return await compress(imageBitmap, targetSize, errorMargin, low, mid, bestBlob)
return await compress(imageBitmap, targetSize, low, mid, bestBlob, threshold)
} else {
return await compress(imageBitmap, targetSize, errorMargin, mid, high, bestBlob)
return await compress(imageBitmap, targetSize, mid, high, bestBlob, threshold)
}
}
const compressImage = async (inputBlob: Blob, targetSize: number, errorMargin: number = 1024) => {
const compressImage = async (inputBlob: Blob, targetSize: number, threshold: number = 1024) => {
// If the original size already meets the target size, return the original Blob
if (inputBlob.size <= targetSize) {
return inputBlob
}
const low = 0.1
const high = 0.9
const bestBlob = inputBlob
// Initialize the range of quality
const low = 0
const high = 1
const imageBitmap = await createImageBitmap(inputBlob)
return await compress(imageBitmap, targetSize, errorMargin, low, high, bestBlob)
// Call the recursive function
return await compress(imageBitmap, targetSize, low, high, inputBlob, threshold)
}
export default compressImage

View file

@ -0,0 +1,102 @@
const generateRandomName = (): string => {
const firstNames = [
'Alex',
'Blake',
'Casey',
'Dana',
'Eden',
'Frankie',
'Gray',
'Harper',
'Indigo',
'Jordan',
'Kennedy',
'Logan',
'Morgan',
'Noah',
'Parker',
'Quinn',
'Riley',
'Sage',
'Taylor',
'Avery',
'Bella',
'Charlie',
'Daisy',
'Ella',
'Fiona',
'Grace',
'Hannah',
'Ivy',
'Jasmine',
'Katherine',
'Luna',
'Mia',
'Nina',
'Olivia',
'Penelope',
'Quinn',
'Riley',
'Sage',
'Taylor',
'Avery',
'Bella',
'Charlie',
'Daisy',
'Ella'
]
const lastNames = [
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Garcia',
'Miller',
'Davis',
'Rodriguez',
'Martinez',
'Hernandez',
'Lopez',
'Gonzalez',
'Wilson',
'Anderson',
'Thomas',
'Taylor',
'Moore',
'Jackson',
'Martin',
'Nguyen',
'Ochoa',
'Perez',
'Quintero',
'Rivera',
'Santos',
'Torres',
'Vargas',
'Wright',
'Xiong',
'Yang',
'Zhao',
'Zhang',
'Li',
'Wang',
'Chen',
'Liu',
'Yang',
'Zhou',
'Wu',
'Lin',
'Lu',
'Zheng',
'Huang'
]
const randomFirstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const randomLastName = lastNames[Math.floor(Math.random() * lastNames.length)]
return `${randomFirstName} ${randomLastName}`
}
export default generateRandomName

View file

@ -3,6 +3,7 @@
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"allowJs": true,
"paths": {
"@/*": ["./src/*"]
}