feat: use ualy avatar
This commit is contained in:
parent
ad2278f5ba
commit
89e20a65db
23 changed files with 1718 additions and 482 deletions
|
@ -24,7 +24,7 @@ export default [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**']
|
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**', '**/lib/**']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
@ -63,11 +63,11 @@
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-react": "^0.350.0",
|
"lucide-react": "^0.350.0",
|
||||||
"nanoid": "^5.0.6",
|
"nanoid": "^5.0.6",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.0",
|
"react-hook-form": "^7.51.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-nice-avatar": "^1.5.0",
|
|
||||||
"react-use": "^17.5.0",
|
"react-use": "^17.5.0",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
"remesh-logger": "^4.1.0",
|
"remesh-logger": "^4.1.0",
|
||||||
"remesh-react": "^4.1.2",
|
"remesh-react": "^4.1.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sonner": "^1.4.3",
|
"sonner": "^1.5.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"trystero": "^0.20.0",
|
"trystero": "^0.20.0",
|
||||||
"type-fest": "^4.11.1",
|
"type-fest": "^4.11.1",
|
||||||
|
|
693
pnpm-lock.yaml
693
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,19 @@ import Footer from '@/app/content/views/Footer'
|
||||||
import Main from '@/app/content/views/Main'
|
import Main from '@/app/content/views/Main'
|
||||||
import AppButton from '@/app/content/views/AppButton'
|
import AppButton from '@/app/content/views/AppButton'
|
||||||
import AppContainer from '@/app/content/views/AppContainer'
|
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() {
|
export default function App() {
|
||||||
|
const send = useRemeshSend()
|
||||||
|
const roomDomain = useRemeshDomain(RoomDomain())
|
||||||
|
|
||||||
|
send(roomDomain.command.JoinRoomCommand(hostRoomId))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
|
@ -13,6 +24,7 @@ export default function App() {
|
||||||
<Footer />
|
<Footer />
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
<AppButton></AppButton>
|
<AppButton></AppButton>
|
||||||
|
<Toaster richColors offset="104px" position="top-center"></Toaster>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import App from './App'
|
||||||
import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
||||||
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
||||||
import '@/assets/styles/tailwind.css'
|
import '@/assets/styles/tailwind.css'
|
||||||
|
import { createElement } from '@/utils'
|
||||||
|
|
||||||
export default defineContentScript({
|
export default defineContentScript({
|
||||||
cssInjectionMode: 'ui',
|
cssInjectionMode: 'ui',
|
||||||
|
@ -26,8 +27,7 @@ export default defineContentScript({
|
||||||
// anchor: 'body',
|
// anchor: 'body',
|
||||||
// append: 'first',
|
// append: 'first',
|
||||||
onMount: (container) => {
|
onMount: (container) => {
|
||||||
const app = document.createElement('div')
|
const app = createElement('<div id="app"></div>')
|
||||||
app.id = 'app'
|
|
||||||
container.append(app)
|
container.append(app)
|
||||||
|
|
||||||
const root = createRoot(app)
|
const root = createRoot(app)
|
||||||
|
|
|
@ -11,6 +11,9 @@ const Header: FC = () => {
|
||||||
const siteInfo = getSiteInfo()
|
const siteInfo = getSiteInfo()
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const roomDomain = useRemeshDomain(RoomDomain())
|
||||||
const peerList = useRemeshQuery(roomDomain.query.PeerListQuery())
|
const peerList = useRemeshQuery(roomDomain.query.PeerListQuery())
|
||||||
|
// const peerList = ['1', '2', '3']
|
||||||
|
console.log('peerList', peerList)
|
||||||
|
console.log(111)
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||||
|
|
||||||
import MessageList from '../../components/MessageList'
|
import MessageList from '../../components/MessageList'
|
||||||
import MessageItem from '../../components/MessageItem'
|
import MessageItem from '../../components/MessageItem'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import RoomDomain from '@/domain/Room'
|
import RoomDomain from '@/domain/Room'
|
||||||
|
|
||||||
|
@ -11,8 +10,7 @@ const Main: FC = () => {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const roomDomain = useRemeshDomain(RoomDomain())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
const _messageList = useRemeshQuery(roomDomain.query.MessageListQuery())
|
||||||
const _messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
const messageList = _messageList.map((message) => ({
|
const messageList = _messageList.map((message) => ({
|
||||||
...message,
|
...message,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export interface AvatarSelectProps {
|
||||||
className?: string
|
className?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
compressSize?: number
|
compressSize?: number
|
||||||
onSuccess?: (blob: Blob) => void
|
onSuccess?: (blob: string) => void
|
||||||
onWarning?: (error: Error) => void
|
onWarning?: (error: Error) => void
|
||||||
onError?: (error: Error) => void
|
onError?: (error: Error) => void
|
||||||
onChange?: (src: string) => void
|
onChange?: (src: string) => void
|
||||||
|
@ -34,8 +34,9 @@ const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
|
||||||
const blob = await compressImage(file, compressSize)
|
const blob = await compressImage(file, compressSize)
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
onSuccess?.(blob)
|
const base64 = e.target?.result as string
|
||||||
onChange?.(e.target?.result as string)
|
onSuccess?.(base64)
|
||||||
|
onChange?.(base64)
|
||||||
}
|
}
|
||||||
reader.onerror = () => onError?.(new Error('Failed to read image file.'))
|
reader.onerror = () => onError?.(new Error('Failed to read image file.'))
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
|
|
|
@ -10,13 +10,15 @@ import { Button } from '@/components/ui/Button'
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import UserInfoDomain, { type UserInfo } from '@/domain/UserInfo'
|
import UserInfoDomain, { type UserInfo } from '@/domain/UserInfo'
|
||||||
import { checkSystemDarkMode } from '@/utils'
|
import { checkSystemDarkMode, compressImage } from '@/utils'
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
|
||||||
import { Label } from '@/components/ui/Label'
|
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
|
// 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%.
|
// 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 = {
|
const defaultUserInfo: UserInfo = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
|
@ -80,6 +82,34 @@ const ProfileForm = () => {
|
||||||
toast.error(error.message)
|
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 (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} autoComplete="off" className="relative w-96 space-y-8 p-10">
|
<form onSubmit={form.handleSubmit(handleSubmit)} autoComplete="off" className="relative w-96 space-y-8 p-10">
|
||||||
|
@ -89,6 +119,7 @@ const ProfileForm = () => {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="absolute left-1/2 top-0 grid -translate-x-1/2 -translate-y-1/2 justify-items-center">
|
<FormItem className="absolute left-1/2 top-0 grid -translate-x-1/2 -translate-y-1/2 justify-items-center">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
<div className="grid justify-items-center gap-y-2">
|
||||||
<AvatarSelect
|
<AvatarSelect
|
||||||
compressSize={COMPRESS_SIZE}
|
compressSize={COMPRESS_SIZE}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
|
@ -96,11 +127,22 @@ const ProfileForm = () => {
|
||||||
className="shadow-lg"
|
className="shadow-lg"
|
||||||
{...field}
|
{...field}
|
||||||
></AvatarSelect>
|
></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>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
|
|
29
src/components/ui/Sonner.tsx
Normal file
29
src/components/ui/Sonner.tsx
Normal 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 }
|
|
@ -4,7 +4,7 @@ import { type MessageUser } from './MessageList'
|
||||||
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import { callbackToObservable, desert, stringToHex } from '@/utils'
|
import { callbackToObservable, desert } from '@/utils'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
|
@ -31,26 +31,41 @@ export interface TextMessage extends MessageUser {
|
||||||
|
|
||||||
export type RoomMessage = LikeMessage | HateMessage | TextMessage
|
export type RoomMessage = LikeMessage | HateMessage | TextMessage
|
||||||
|
|
||||||
const hostRoomId = stringToHex(document.location.host)
|
|
||||||
|
|
||||||
const RoomDomain = Remesh.domain({
|
const RoomDomain = Remesh.domain({
|
||||||
name: 'RoomDomain',
|
name: 'RoomDomain',
|
||||||
impl: (domain) => {
|
impl: (domain) => {
|
||||||
const messageListDomain = domain.getDomain(MessageListDomain())
|
const messageListDomain = domain.getDomain(MessageListDomain())
|
||||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||||
const peerRoom = domain.getExtern(PeerRoomExtern)
|
const peerRoom = domain.getExtern(PeerRoomExtern)
|
||||||
peerRoom.joinRoom(hostRoomId)
|
|
||||||
|
|
||||||
const PeersListState = domain.state<string[]>({
|
const MessageListQuery = messageListDomain.query.ListQuery
|
||||||
name: 'Room.PeersListState',
|
|
||||||
|
const PeerListState = domain.state<string[]>({
|
||||||
|
name: 'Room.PeerListState',
|
||||||
default: [peerRoom.selfId]
|
default: [peerRoom.selfId]
|
||||||
})
|
})
|
||||||
|
|
||||||
const MessageListQuery = messageListDomain.query.ListQuery
|
|
||||||
const PeerListQuery = domain.query({
|
const PeerListQuery = domain.query({
|
||||||
name: 'Room.PeerListQuery',
|
name: 'Room.PeerListQuery',
|
||||||
impl: ({ get }) => {
|
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 [
|
return [
|
||||||
messageListDomain.command.UpdateItemCommand({
|
messageListDomain.command.UpdateItemCommand({
|
||||||
..._message,
|
..._message,
|
||||||
likeUsers: desert(_message.likeUsers, 'userId', {
|
likeUsers: desert(
|
||||||
|
_message.likeUsers,
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
userAvatar
|
userAvatar
|
||||||
})
|
},
|
||||||
|
'userId'
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
SendLikeMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Like })
|
SendLikeMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Like })
|
||||||
]
|
]
|
||||||
|
@ -111,11 +130,15 @@ const RoomDomain = Remesh.domain({
|
||||||
return [
|
return [
|
||||||
messageListDomain.command.UpdateItemCommand({
|
messageListDomain.command.UpdateItemCommand({
|
||||||
..._message,
|
..._message,
|
||||||
hateUsers: desert(_message.hateUsers, 'userId', {
|
hateUsers: desert(
|
||||||
|
_message.hateUsers,
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
username,
|
username,
|
||||||
userAvatar
|
userAvatar
|
||||||
})
|
},
|
||||||
|
'userId'
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Hate })
|
SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: MessageType.Hate })
|
||||||
]
|
]
|
||||||
|
@ -134,10 +157,24 @@ const RoomDomain = Remesh.domain({
|
||||||
name: 'RoomLeaveRoomEvent'
|
name: 'RoomLeaveRoomEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const SyncPeersListCommand = domain.command({
|
const UpdatePeerListCommand = domain.command({
|
||||||
name: 'RoomSyncPeersListCommand',
|
name: 'RoomUpdatePeerListCommand',
|
||||||
impl: (_, list: string[]) => {
|
impl: ({ get }, action: { type: 'create' | 'delete'; peerId: string }) => {
|
||||||
return [PeersListState().new(list)]
|
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))
|
const _message = get(messageListDomain.query.ItemQuery(message.id))
|
||||||
return messageListDomain.command.UpdateItemCommand({
|
return messageListDomain.command.UpdateItemCommand({
|
||||||
..._message,
|
..._message,
|
||||||
likeUsers: desert(_message.likeUsers, 'userId', {
|
likeUsers: desert(
|
||||||
|
_message.likeUsers,
|
||||||
|
{
|
||||||
userId: message.userId,
|
userId: message.userId,
|
||||||
username: message.username,
|
username: message.username,
|
||||||
userAvatar: message.userAvatar
|
userAvatar: message.userAvatar
|
||||||
})
|
},
|
||||||
|
'userId'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case 'hate': {
|
case 'hate': {
|
||||||
|
@ -212,11 +253,15 @@ const RoomDomain = Remesh.domain({
|
||||||
const _message = get(messageListDomain.query.ItemQuery(message.id))
|
const _message = get(messageListDomain.query.ItemQuery(message.id))
|
||||||
return messageListDomain.command.UpdateItemCommand({
|
return messageListDomain.command.UpdateItemCommand({
|
||||||
..._message,
|
..._message,
|
||||||
hateUsers: desert(_message.hateUsers, 'userId', {
|
hateUsers: desert(
|
||||||
|
_message.hateUsers,
|
||||||
|
{
|
||||||
userId: message.userId,
|
userId: message.userId,
|
||||||
username: message.username,
|
username: message.username,
|
||||||
userAvatar: message.userAvatar
|
userAvatar: message.userAvatar
|
||||||
})
|
},
|
||||||
|
'userId'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -230,11 +275,12 @@ const RoomDomain = Remesh.domain({
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnJoinRoomEffect',
|
name: 'RoomOnJoinRoomEffect',
|
||||||
impl: ({ get }) => {
|
impl: () => {
|
||||||
const onJoinRoom$ = callbackToObservable<string>(peerRoom.onJoinRoom.bind(peerRoom))
|
const onJoinRoom$ = callbackToObservable<string>(peerRoom.onJoinRoom.bind(peerRoom))
|
||||||
return onJoinRoom$.pipe(
|
return onJoinRoom$.pipe(
|
||||||
map((peerId) => {
|
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({
|
domain.effect({
|
||||||
name: 'RoomOnLeaveRoomEffect',
|
name: 'RoomOnLeaveRoomEffect',
|
||||||
impl: ({ get }) => {
|
impl: () => {
|
||||||
const onLeaveRoom$ = callbackToObservable<string>(peerRoom.onLeaveRoom.bind(peerRoom))
|
const onLeaveRoom$ = callbackToObservable<string>(peerRoom.onLeaveRoom.bind(peerRoom))
|
||||||
return onLeaveRoom$.pipe(
|
return onLeaveRoom$.pipe(
|
||||||
map((peerId) => {
|
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
|
LeaveRoomEvent
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
|
JoinRoomCommand,
|
||||||
|
LeaveRoomCommand,
|
||||||
SendTextMessageCommand,
|
SendTextMessageCommand,
|
||||||
SendLikeMessageCommand,
|
SendLikeMessageCommand,
|
||||||
SendHateMessageCommand
|
SendHateMessageCommand
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Remesh } from 'remesh'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { BrowserSyncStorageExtern } from '@/domain/externs/Storage'
|
import { BrowserSyncStorageExtern } from '@/domain/externs/Storage'
|
||||||
import StorageEffect from '@/domain/modules/StorageEffect'
|
import StorageEffect from '@/domain/modules/StorageEffect'
|
||||||
|
import generateUglyAvatar from '@/lib/uglyAvatar'
|
||||||
|
import generateRandomName from '@/utils/generateRandomName'
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
id: string
|
id: string
|
||||||
|
@ -24,14 +26,7 @@ const UserInfoDomain = Remesh.domain({
|
||||||
|
|
||||||
const UserInfoState = domain.state<UserInfo | null>({
|
const UserInfoState = domain.state<UserInfo | null>({
|
||||||
name: 'UserInfo.UserInfoState',
|
name: 'UserInfo.UserInfoState',
|
||||||
// defer: true
|
default: null
|
||||||
default: {
|
|
||||||
id: nanoid(),
|
|
||||||
name: '游客',
|
|
||||||
avatar: 'https://avatars.githubusercontent.com/u/10354233?v=4',
|
|
||||||
createTime: Date.now(),
|
|
||||||
themeMode: 'system'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserInfoQuery = domain.query({
|
const UserInfoQuery = domain.query({
|
||||||
|
@ -80,10 +75,10 @@ const UserInfoDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// storageEffect
|
storageEffect
|
||||||
// .set(SyncToStorageEvent)
|
.set(SyncToStorageEvent)
|
||||||
// .get<UserInfo>((value) => SyncToStateCommand(value))
|
.get<UserInfo>((value) => SyncToStateCommand(value))
|
||||||
// .watch<UserInfo>((value) => SyncToStateCommand(value))
|
.watch<UserInfo>((value) => SyncToStateCommand(value!))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
|
|
|
@ -21,12 +21,6 @@ class PeerRoom {
|
||||||
async joinRoom(roomId: string) {
|
async joinRoom(roomId: string) {
|
||||||
this.room = joinRoom({ appId: this.appId }, roomId)
|
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
|
return this.room
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
71
src/domain/modules/Toast.ts
Normal file
71
src/domain/modules/Toast.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
src/lib/uglyAvatar/eye_shape.js
Normal file
163
src/lib/uglyAvatar/eye_shape.js
Normal 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 }
|
||||||
|
}
|
211
src/lib/uglyAvatar/face_shape.js
Normal file
211
src/lib/uglyAvatar/face_shape.js
Normal 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] };
|
||||||
|
}
|
152
src/lib/uglyAvatar/hair_lines.js
Normal file
152
src/lib/uglyAvatar/hair_lines.js
Normal 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
260
src/lib/uglyAvatar/index.js
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
/**
|
||||||
|
* Ported from the ugly-avatar project
|
||||||
|
* Repo:https://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
|
||||||
|
}
|
171
src/lib/uglyAvatar/mouth_shape.js
Normal file
171
src/lib/uglyAvatar/mouth_shape.js
Normal 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;
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
export const chunk = <T = any>(array: T[], size: number) =>
|
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))
|
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) => {
|
export const desert = <T = any>(target: T[], value: T, key?: keyof T) => {
|
||||||
const index = target.findIndex((item) => item[key] === value[key])
|
const index = target.findIndex((item) => (key ? item[key] === value[key] : value === item))
|
||||||
return index === -1 ? [...target, value] : target.toSpliced(index, 1)
|
return index === -1 ? [...target, value] : target.toSpliced(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const upsert = <T extends object>(target: T[], key: keyof T, value: T) => {
|
export const upsert = <T = any>(target: T[], value: T, key?: keyof T) => {
|
||||||
const index = target.findIndex((item) => item[key] === value[key])
|
const index = target.findIndex((item) => (key ? item[key] === value[key] : value === item))
|
||||||
return index === -1 ? [...target, value] : target.toSpliced(index, 1, value)
|
return index === -1 ? [...target, value] : target.toSpliced(index, 1, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
const compress = async (
|
||||||
imageBitmap: ImageBitmap,
|
imageBitmap: ImageBitmap,
|
||||||
targetSize: number,
|
targetSize: number,
|
||||||
errorMargin: number,
|
|
||||||
low: number,
|
low: number,
|
||||||
high: number,
|
high: number,
|
||||||
bestBlob: Blob
|
bestBlob: Blob,
|
||||||
|
threshold: number
|
||||||
): Promise<Blob> => {
|
): Promise<Blob> => {
|
||||||
|
// Calculate the middle value of quality
|
||||||
const mid = (low + high) / 2
|
const mid = (low + high) / 2
|
||||||
|
|
||||||
|
// Calculate the width and height after scaling
|
||||||
const width = imageBitmap.width * mid
|
const width = imageBitmap.width * mid
|
||||||
const height = imageBitmap.height * mid
|
const height = imageBitmap.height * mid
|
||||||
|
|
||||||
const offscreenCanvas = new OffscreenCanvas(width, height)
|
const offscreenCanvas = new OffscreenCanvas(width, height)
|
||||||
const offscreenContext = offscreenCanvas.getContext('2d')!
|
const offscreenContext = offscreenCanvas.getContext('2d')!
|
||||||
|
|
||||||
offscreenContext.drawImage(imageBitmap, 0, 0, width, height)
|
offscreenContext.drawImage(imageBitmap, 0, 0, width, height)
|
||||||
|
|
||||||
const outputBlob = await offscreenCanvas.convertToBlob({ type: 'image/jpeg', quality: mid })
|
const outputBlob = await offscreenCanvas.convertToBlob({ type: 'image/jpeg', quality: mid })
|
||||||
|
|
||||||
|
// Calculate the current size based on the current quality
|
||||||
const currentSize = outputBlob.size
|
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
|
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
|
return bestBlob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adjust the range for recursion based on the current quality and size
|
||||||
if (currentSize > targetSize) {
|
if (currentSize > targetSize) {
|
||||||
return await compress(imageBitmap, targetSize, errorMargin, low, mid, bestBlob)
|
return await compress(imageBitmap, targetSize, low, mid, bestBlob, threshold)
|
||||||
} else {
|
} 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) {
|
if (inputBlob.size <= targetSize) {
|
||||||
return inputBlob
|
return inputBlob
|
||||||
}
|
}
|
||||||
|
|
||||||
const low = 0.1
|
// Initialize the range of quality
|
||||||
const high = 0.9
|
const low = 0
|
||||||
const bestBlob = inputBlob
|
const high = 1
|
||||||
|
|
||||||
const imageBitmap = await createImageBitmap(inputBlob)
|
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
|
export default compressImage
|
||||||
|
|
102
src/utils/generateRandomName.ts
Normal file
102
src/utils/generateRandomName.ts
Normal 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
|
|
@ -3,6 +3,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue