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: {
|
||||
|
|
|
@ -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",
|
||||
|
|
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 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,6 +119,7 @@ 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>
|
||||
<div className="grid justify-items-center gap-y-2">
|
||||
<AvatarSelect
|
||||
compressSize={COMPRESS_SIZE}
|
||||
onError={handleError}
|
||||
|
@ -96,11 +127,22 @@ const ProfileForm = () => {
|
|||
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"
|
||||
|
|
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 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', {
|
||||
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', {
|
||||
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', {
|
||||
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', {
|
||||
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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
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) =>
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
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": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue