reng 2 months ago
commit 91a23c3a35
  1. 24
      .gitignore
  2. 16
      README.md
  3. 29
      eslint.config.js
  4. 13
      firebase.json
  5. 18
      index.html
  6. 3512
      package-lock.json
  7. 31
      package.json
  8. 3
      public/back.svg
  9. 320
      public/data.json
  10. 62
      public/egg_bg.svg
  11. 6
      public/eye.svg
  12. 39
      public/hint.svg
  13. 18
      public/icon.svg
  14. 4
      public/letter.svg
  15. BIN
      public/main.png
  16. 3
      public/topic.svg
  17. 3
      public/topic_select.svg
  18. 4
      public/x.svg
  19. 0
      src/App.css
  20. 23
      src/App.jsx
  21. 1
      src/assets/react.svg
  22. 23
      src/comps/button.jsx
  23. 225
      src/comps/canvas.jsx
  24. 24
      src/index.css
  25. 35
      src/main.jsx
  26. 25
      src/pages/about.jsx
  27. 92
      src/pages/explore.jsx
  28. 62
      src/pages/keywords.jsx
  29. 0
      src/utils/useSelect.jsx
  30. 67
      src/utils/usetext.jsx
  31. 7
      vite.config.js

24
.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

@ -0,0 +1,13 @@
{
"hosting": {
"source": ".",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"frameworksBackend": {
"region": "asia-east1"
}
}
}

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GRAYSCALE</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100..900&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3512
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,31 @@
{
"name": "source-web-23070-grayscale",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"swr": "^2.4.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.2.4"
}
}

@ -0,0 +1,3 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.70715 0.353516L0.707153 9.35352L9.70715 18.3535" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 181 B

@ -0,0 +1,320 @@
{
"title": {
"en": "GRAYSCALE",
"zh": "灰度"
},
"button_enter": {
"en": "enter",
"zh": "進入"
},
"button_explore": {
"en": "explore",
"zh": "開始探索"
},
"button_reselect": {
"en": "reselect",
"zh": "重新選擇"
},
"title_keywords": {
"en": "Select 4 signals to observe.",
"zh": "選擇四個想觀測的訊號"
},
"title_explore": {
"en": "Observed the dialogues in the installation.",
"zh": "觀測儀器裡的對話"
},
"title_observe": {
"en": "Observe the installation...",
"zh": "觀測儀器..."
},
"title_about": {
"en": "About",
"zh": "關於作品"
},
"content_about": {
"en": "What defines an \"adult\"? In reality, the standards are never consistent. We are expected to be mature and responsible, yet we are often treated as individuals who are not quite \"ready\". This unclassifiable state is our shared experience.\nThe visuals here are culled from fragmented social media discussions on career, age, and social norms. Though scattered, they all point to the same \"gray dilemma\" occurring everywhere.\nThrough AI, these isolated voices are aggregated into this single device. It offers no answers, but forms a new shape from these dialogues—inviting you to step closer and observe.",
"zh": "作品源於 2025 年陽明交大應藝所、建築所與害喜影音綜藝共同發起的一場實踐。相較於校園中常見的傳統公共藝術,我們試圖從學習者的視角出發,將那些發生在校園裡的迷惘與不確定,變成一場關於公共性的思辨。\n我們在思考:究竟什麼是「長大成人」?在生活中,標準從未真正一致。隨著年紀增長,人被期待表現成熟、負責,但又常常在社會中,被視為還沒準備好的個體。這種難以歸類的狀態,構成共同經驗。\n你所看到的影像,來自許多人在社群平台上,對於年紀、職場、生涯規劃與社會規範留下的討論。這些發言短暫、零散,卻指向同一種灰色的困境——不論幾歲、不論在哪,都可能正發生。\n透過 AI 與數位,這些原本彼此獨立的討論被放在同一個裝置裡。作品並非試圖給出答案,而是將這些對話聚集在一起,形成一個新的形狀,邀請你靠近觀察。"
},
"title_credits": {
"en": "Credit",
"zh": "製作團隊"
},
"content_credits": {
"en": "Artwork by ULTRACOMBOS",
"zh": "Artwork by ULTRACOMBOS"
},
"title_share": {
"en": "SHARE YOUR THOUGHTS",
"zh": "分享你的想法"
},
"keywords": {
"en": [
{
"title": "Overworked",
"content": "How much of your schedule is actually for yourself?"
},
{
"title": "Internal Burnout",
"content": "Do you spend a lot of energy just managing your own emotions?"
},
{
"title": "Student Loans",
"content": "How many years will it take to pay off that diploma?"
},
{
"title": "Renting",
"content": "What slice of your paycheck goes straight to the landlord?"
},
{
"title": "Debt",
"content": "Do you feel like you owe anyone \"time\" or \"favors\"?"
},
{
"title": "Adulthood",
"content": "What’s the gap between your legal age and how old you actually feel?"
},
{
"title": "Survival",
"content": "Are you living right now, or just surviving?"
},
{
"title": "Discipline",
"content": "Which social habits were you born with, and which did you learn to fit in?"
},
{
"title": "PUA",
"content": "Is the self-doubt coming from the task, or someone else’s judgment?"
},
{
"title": "Consumable",
"content": "If you took tomorrow off, would the system really stop?"
},
{
"title": "Servility",
"content": "What’s your gut reaction to an unreasonable order?"
},
{
"title": "Unwritten Rules",
"content": "Which unspoken rules are you quietly following?"
},
{
"title": "Corporate Slave",
"content": "How do you tell the \"work you\" apart from the \"real you\"?"
},
{
"title": "Curfew",
"content": "When is your most comfortable time to be home?"
},
{
"title": "Boundaries",
"content": "When do you say \"no\" just to protect yourself?"
},
{
"title": "Surveillance",
"content": "Would you act differently if you knew no one was watching?"
},
{
"title": "Inside the Wall",
"content": "Does your environment feel like a shield or a cage?"
},
{
"title": "Constriction",
"content": "What’s the biggest change you made just to adapt?"
},
{
"title": "Freedom",
"content": "What does \"freedom\" actually look like to you?"
},
{
"title": "Awakening",
"content": "Do you accept your \"factory settings\"?"
},
{
"title": "Lying Flat",
"content": "Are you resting to go further, or because the road has ended?"
},
{
"title": "Fighting Back",
"content": "When was the last time you spoke up or said \"no\"?"
},
{
"title": "Subjectivity",
"content": "Do you usually speak up, or just go with the flow?"
},
{
"title": "Undefined",
"content": "Describe yourself in three words without using your job title."
},
{
"title": "Being Yourself",
"content": "How much of your true self do you show in public?"
},
{
"title": "Finding Gaps",
"content": "How do you find a little breathing room in your busy life?"
},
{
"title": "Suffocating",
"content": "Which topics or places make you feel like you can't breathe?"
},
{
"title": "Who Gets It",
"content": "Who in your life truly understands what you’re going through?"
},
{
"title": "Anxiety",
"content": "Are you worried about the future, or stuck in the past?"
},
{
"title": "World-weary",
"content": "Is this feeling just pure fatigue from the way things are?"
},
{
"title": "Irony",
"content": "Have the rules you once hated become your survival instincts?"
},
{
"title": "Empathy",
"content": "Which words in this device said what you couldn't say out loud?"
},
{
"title": "Illusion",
"content": "Are your goals truly yours, or just what society wants for you?"
}
],
"zh": [
{
"title": "窮忙",
"content": "你今天的行程裡,有多少比例是為了自己?"
},
{
"title": "內耗",
"content": "你會花很多力氣處理自己的情緒嗎?"
},
{
"title": "學貸",
"content": "一張畢業證書要花多少年來分期付款?"
},
{
"title": "租屋",
"content": "這個月薪水的幾分之幾交給房東了?"
},
{
"title": "負債",
"content": "你覺得自己現在有欠下「時間」或「人情」嗎?"
},
{
"title": "成年",
"content": "法律定義的成年與你心理感受的成年,中間的距離有多少?"
},
{
"title": "求生存",
"content": "你會說你現在在過生活還是求生存?"
},
{
"title": "規訓",
"content": "你所遵守的社交習慣,哪些是天生的、哪些是後來學會的?"
},
{
"title": "PUA",
"content": "讓你自我懷疑的是事情本身,還是某人的評價?"
},
{
"title": "耗材",
"content": "如果你明天突然休息,系統真的會因此停擺嗎?"
},
{
"title": "奴性",
"content": "在面對不合理的指令時,你的第一反應通常是什麼?"
},
{
"title": "潛規則",
"content": "有哪些規則是沒被寫出來,但你卻正在默默遵守的?"
},
{
"title": "社畜",
"content": "你如何區分「工作時的你」與「下班後的你」?"
},
{
"title": "門禁",
"content": "覺得最舒適的回家時間是什麼時候?"
},
{
"title": "邊界",
"content": "你什麼時候會說不,來保護自己?"
},
{
"title": "監控",
"content": "如果沒人看見,你的行為表現會和現在有很大的差異嗎?"
},
{
"title": "牆內",
"content": "你覺得現在的環境帶給你的是「保護感」還是「限制感」?"
},
{
"title": "限縮",
"content": "為了適應環境,你做出的最大調整是什麼?"
},
{
"title": "自由",
"content": "對你來說,什麼樣的狀態才叫「自由」?"
},
{
"title": "覺醒",
"content": "你接受自己的出廠設定嗎?"
},
{
"title": "躺平",
"content": "休息是因為想走更長的路,還是因為路已經走不下去了?"
},
{
"title": "反擊",
"content": "你曾為什麼事,大聲說出不滿或拒絕?"
},
{
"title": "主體性",
"content": "在群體中,你通常是提出意見的人還是配合的人?"
},
{
"title": "不定義",
"content": "如果不使用職稱,你會用哪三個詞來描述自己?"
},
{
"title": "做自己",
"content": "在公共場所,你覺得自己有多少程度能呈現真實的樣貌?"
},
{
"title": "找縫隙",
"content": "在繁忙的節奏中,你最常用什麼方式找回自己的空間?"
},
{
"title": "窒息",
"content": "在什麼樣的環境或話題中,你會感到最明顯的壓迫感?"
},
{
"title": "誰懂",
"content": "你覺得目前身邊的人,誰真正理解你的處境?"
},
{
"title": "焦慮",
"content": "你在擔心還沒發生的未來,還是後悔已經發生的過去?"
},
{
"title": "厭世",
"content": "厭世是對現狀的疲憊嗎?"
},
{
"title": "諷刺",
"content": "在荒謬的日常中,你如何用幽默來回應生活的無奈?"
},
{
"title": "共感",
"content": "裝置中哪些話是你說不出口,但有人替你說了?"
},
{
"title": "幻覺",
"content": "你正在追求的事,有多少是你的願望,有多少是社會的投射?"
}
]
}
}

@ -0,0 +1,62 @@
<svg width="393" height="500" viewBox="0 0 393 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4086_762)">
<path d="M367.078 124.284C422.428 200.624 390.567 318.357 295.596 387.217C200.626 456.076 78.8206 449.76 23.4697 373.421C-31.8812 297.081 -0.0202878 179.347 94.9505 110.487C189.921 41.6279 311.727 47.9446 367.078 124.284Z" stroke="white"/>
<mask id="mask0_4086_762" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-43" y="26" width="479" height="448">
<path d="M402.571 100.584C469.2 192.479 430.837 333.953 316.884 416.576C202.931 499.199 56.5407 491.683 -10.0888 399.788C-76.7182 307.894 -38.3551 166.419 75.5977 83.7958C189.551 1.1731 335.942 8.68976 402.571 100.584ZM377.792 118.551C319.284 37.8578 190.571 31.3783 90.3029 104.079C-9.96468 176.779 -43.8177 301.129 14.6898 381.822C73.1974 462.516 201.91 468.995 302.178 396.295C402.446 323.595 436.299 199.244 377.792 118.551Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_4086_762)">
<path d="M-32.0911 416.25L416.96 90.6605" stroke="white"/>
<path d="M-46.4344 394.669L431.305 112.242" stroke="white"/>
<path d="M-58.9603 372.012L443.832 134.898" stroke="white"/>
<path d="M-69.5732 348.453L454.445 158.456" stroke="white"/>
<path d="M-78.1912 324.173L463.066 182.738" stroke="white"/>
<path d="M-84.75 299.352L469.626 207.557" stroke="white"/>
<path d="M-89.1996 274.183L474.076 232.725" stroke="white"/>
<path d="M-85.47 173.855L470.349 333.055" stroke="white"/>
<path d="M-91.506 248.857L476.382 258.053" stroke="white"/>
<path d="M-91.6516 223.565L476.527 283.344" stroke="white"/>
<path d="M-89.6342 198.501L474.512 308.409" stroke="white"/>
<path d="M227.892 -31.7226L156.983 538.639" stroke="white"/>
<path d="M203.173 -35.4273L181.703 542.343" stroke="white"/>
<path d="M178.371 -36.9332L206.503 543.848" stroke="white"/>
<path d="M153.678 -36.2293L231.197 543.144" stroke="white"/>
<path d="M129.279 -33.3203L255.596 540.235" stroke="white"/>
<path d="M105.361 -28.2285L279.514 535.144" stroke="white"/>
<path d="M59.6896 -11.6688L325.185 518.585" stroke="white"/>
<path d="M252.341 -25.847L132.534 532.765" stroke="white"/>
<path d="M276.334 -17.8459L108.54 524.765" stroke="white"/>
<path d="M299.689 -7.78077L85.1849 514.7" stroke="white"/>
<path d="M322.227 4.27379L62.6459 502.647" stroke="white"/>
<path d="M343.777 18.2243L41.0944 488.698" stroke="white"/>
<path d="M364.176 33.9649L20.6949 472.958" stroke="white"/>
<path d="M431.305 112.249L-46.4348 394.676" stroke="white"/>
<path d="M383.269 51.3757L1.60339 455.548" stroke="white"/>
<path d="M400.908 70.3237L-16.0366 436.598" stroke="white"/>
<path d="M416.961 90.6673L-32.0902 416.257" stroke="white"/>
<path d="M-89.6416 198.503L474.504 308.411" stroke="white"/>
<path d="M-85.4772 173.857L470.342 333.057" stroke="white"/>
<path d="M-79.1979 149.817L464.064 357.097" stroke="white"/>
<path d="M-70.8518 126.565L455.719 380.348" stroke="white"/>
<path d="M-60.5015 104.28L445.37 402.635" stroke="white"/>
<path d="M-48.2266 83.13L433.096 423.786" stroke="white"/>
<path d="M-34.1203 63.2758L418.99 443.64" stroke="white"/>
<path d="M38.2823 -0.325594L346.589 507.244" stroke="white"/>
<path d="M82.1039 -20.9909L302.768 527.911" stroke="white"/>
<path d="M-18.2901 44.8696L403.16 462.047" stroke="white"/>
<path d="M-0.85628 28.05L385.725 478.866" stroke="white"/>
<path d="M18.0496 12.9467L366.821 493.97" stroke="white"/>
<path d="M366.82 493.973L18.0481 12.95" stroke="white"/>
</g>
<mask id="mask1_4086_762" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="-35" y="11" width="470" height="473">
<path d="M389.02 110.41C465.738 216.22 443.294 363.363 338.889 439.063C234.484 514.763 87.6533 490.354 10.9348 384.545C-65.7834 278.735 -43.3386 131.592 61.0663 55.8922C165.471 -19.8079 312.301 4.60047 389.02 110.41ZM380.283 116.745C318.035 30.8931 186.847 19.8272 87.2675 92.0287C-12.3122 164.23 -42.5758 292.358 19.6719 378.21C81.9197 464.062 213.107 475.127 312.687 402.926C412.267 330.724 442.531 202.597 380.283 116.745Z" fill="white"/>
</mask>
<g mask="url(#mask1_4086_762)">
<path d="M-28.3543 413.541L420.697 87.9513" stroke="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_4086_762">
<rect width="393" height="500" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@ -0,0 +1,6 @@
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.08301 0.5C4.95888 0.5 5.84498 1.112 6.53906 2.30176C7.22601 3.47938 7.66699 5.13962 7.66699 7C7.66699 8.86038 7.22601 10.5206 6.53906 11.6982C5.84498 12.888 4.95888 13.5 4.08301 13.5C3.20728 13.4998 2.32192 12.8878 1.62793 11.6982C0.940985 10.5206 0.5 8.86038 0.5 7C0.5 5.13962 0.940985 3.47938 1.62793 2.30176C2.32192 1.11224 3.20728 0.500154 4.08301 0.5Z" fill="black" stroke="white"/>
<path d="M11.8607 0.5C12.7366 0.5 13.6227 1.112 14.3168 2.30176C15.0037 3.47938 15.4447 5.13962 15.4447 7C15.4447 8.86038 15.0037 10.5206 14.3168 11.6982C13.6227 12.888 12.7366 13.5 11.8607 13.5C10.985 13.4998 10.0996 12.8878 9.40564 11.6982C8.7187 10.5206 8.27771 8.86038 8.27771 7C8.27771 5.13962 8.7187 3.47938 9.40564 2.30176C10.0996 1.11224 10.985 0.500154 11.8607 0.5Z" fill="black" stroke="white"/>
<ellipse cx="5.63894" cy="7.19461" rx="1.36111" ry="1.36111" fill="white"/>
<ellipse cx="13.4168" cy="7.19461" rx="1.36111" ry="1.36111" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -0,0 +1,39 @@
<svg width="393" height="292" viewBox="0 0 393 292" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M196.5 214.934C243.152 214.934 285.377 217.675 315.93 222.104C331.209 224.32 343.548 226.954 352.057 229.87C356.314 231.329 359.583 232.85 361.777 234.404C363.989 235.971 365 237.492 365 238.934C365 240.376 363.989 241.896 361.777 243.463C359.583 245.018 356.314 246.538 352.057 247.997C343.548 250.913 331.209 253.548 315.93 255.763C285.377 260.192 243.152 262.934 196.5 262.934C149.848 262.934 107.623 260.192 77.0703 255.763C61.7906 253.548 49.4517 250.913 40.9434 247.997C36.6859 246.538 33.4171 245.018 31.2227 243.463C29.0115 241.896 28 240.376 28 238.934C28 237.492 29.0115 235.971 31.2227 234.404C33.4171 232.85 36.6859 231.329 40.9434 229.87C49.4517 226.954 61.7906 224.32 77.0703 222.104C107.623 217.675 149.848 214.934 196.5 214.934Z" stroke="white"/>
<path d="M186.718 28.4336V98.7236" stroke="white"/>
<path d="M151.5 63.6509L221.79 63.6509" stroke="white"/>
<path d="M161.978 38.9106L211.68 88.6132" stroke="white"/>
<path d="M161.978 88.6816L211.68 38.9791" stroke="white"/>
<path d="M217.017 96.4212C265.113 78.5135 315.213 94.2131 329.096 131.155C342.979 168.097 315.53 212.671 267.434 230.579C219.337 248.487 169.236 232.787 155.353 195.845C141.47 158.903 168.92 114.329 217.017 96.4212Z" fill="black" stroke="white"/>
<g opacity="0.58">
<mask id="mask0_4073_1504" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="151" y="88" width="183" height="151">
<path d="M217.017 96.4212C265.113 78.5135 315.213 94.2131 329.096 131.155C342.979 168.097 315.53 212.671 267.434 230.579C219.337 248.487 169.236 232.787 155.353 195.845C141.47 158.903 168.92 114.329 217.017 96.4212Z" fill="black" stroke="white"/>
</mask>
<g mask="url(#mask0_4073_1504)">
<ellipse cx="221" cy="111.434" rx="101.5" ry="106" fill="url(#paint0_radial_4073_1504)"/>
</g>
</g>
<path d="M338.428 197.088L321.155 214.362" stroke="white"/>
<path d="M321.119 197.088L338.393 214.362" stroke="white"/>
<path d="M329.774 193.583V218.012" stroke="white"/>
<path d="M317.543 205.814H341.971" stroke="white"/>
<mask id="path-13-outside-1_4073_1504" maskUnits="userSpaceOnUse" x="58.5" y="85.2583" width="80" height="158" fill="black">
<rect fill="white" x="58.5" y="85.2583" width="80" height="158"/>
<path d="M117.5 86.2583C128.546 86.2584 137.5 95.2126 137.5 106.258V179.258H127.862V241.776H107.979V179.258H88.0977V241.776H68.2158V179.258H59.5V106.258C59.5 95.2126 68.4543 86.2583 79.5 86.2583H117.5Z"/>
</mask>
<path d="M117.5 86.2583C128.546 86.2584 137.5 95.2126 137.5 106.258V179.258H127.862V241.776H107.979V179.258H88.0977V241.776H68.2158V179.258H59.5V106.258C59.5 95.2126 68.4543 86.2583 79.5 86.2583H117.5Z" fill="black"/>
<path d="M117.5 86.2583L117.5 85.2583H117.5V86.2583ZM137.5 179.258V180.258H138.5V179.258H137.5ZM127.862 179.258V178.258H126.862V179.258H127.862ZM127.862 241.776V242.776H128.862V241.776H127.862ZM107.979 241.776H106.979V242.776H107.979V241.776ZM107.979 179.258H108.979V178.258H107.979V179.258ZM88.0977 179.258V178.258H87.0977V179.258H88.0977ZM88.0977 241.776V242.776H89.0977V241.776H88.0977ZM68.2158 241.776H67.2158V242.776H68.2158V241.776ZM68.2158 179.258H69.2158V178.258H68.2158V179.258ZM59.5 179.258H58.5V180.258H59.5V179.258ZM117.5 86.2583L117.5 87.2583C127.993 87.2584 136.5 95.7649 136.5 106.258H137.5H138.5C138.5 94.6604 129.098 85.2584 117.5 85.2583L117.5 86.2583ZM137.5 106.258H136.5V179.258H137.5H138.5V106.258H137.5ZM137.5 179.258V178.258H127.862V179.258V180.258H137.5V179.258ZM127.862 179.258H126.862V241.776H127.862H128.862V179.258H127.862ZM127.862 241.776V240.776H107.979V241.776V242.776H127.862V241.776ZM107.979 241.776H108.979V179.258H107.979H106.979V241.776H107.979ZM107.979 179.258V178.258H88.0977V179.258V180.258H107.979V179.258ZM88.0977 179.258H87.0977V241.776H88.0977H89.0977V179.258H88.0977ZM88.0977 241.776V240.776H68.2158V241.776V242.776H88.0977V241.776ZM68.2158 241.776H69.2158V179.258H68.2158H67.2158V241.776H68.2158ZM68.2158 179.258V178.258H59.5V179.258V180.258H68.2158V179.258ZM59.5 179.258H60.5V106.258H59.5H58.5V179.258H59.5ZM59.5 106.258H60.5C60.5 95.7649 69.0066 87.2583 79.5 87.2583V86.2583V85.2583C67.902 85.2583 58.5 94.6603 58.5 106.258H59.5ZM79.5 86.2583V87.2583H117.5V86.2583V85.2583H79.5V86.2583Z" fill="white" mask="url(#path-13-outside-1_4073_1504)"/>
<mask id="path-15-outside-2_4073_1504" maskUnits="userSpaceOnUse" x="74.7063" y="27.4336" width="58" height="53" fill="black">
<rect fill="white" x="74.7063" y="27.4336" width="58" height="53"/>
<path d="M101.163 28.4336C115.223 28.4337 126.62 39.8312 126.62 53.8906C126.62 55.5815 126.453 57.2334 126.139 58.832L130.689 66.7129H123.157C118.742 74.2696 110.547 79.3476 101.163 79.3477C87.104 79.3477 75.7065 67.9499 75.7063 53.8906C75.7063 39.8312 87.1039 28.4336 101.163 28.4336Z"/>
</mask>
<path d="M101.163 28.4336C115.223 28.4337 126.62 39.8312 126.62 53.8906C126.62 55.5815 126.453 57.2334 126.139 58.832L130.689 66.7129H123.157C118.742 74.2696 110.547 79.3476 101.163 79.3477C87.104 79.3477 75.7065 67.9499 75.7063 53.8906C75.7063 39.8312 87.1039 28.4336 101.163 28.4336Z" fill="black"/>
<path d="M101.163 28.4336L101.163 27.4336H101.163V28.4336ZM126.62 53.8906L127.62 53.8906V53.8906H126.62ZM126.139 58.832L125.158 58.639L125.085 59.0071L125.273 59.332L126.139 58.832ZM130.689 66.7129V67.7129H132.421L131.555 66.2129L130.689 66.7129ZM123.157 66.7129V65.7129H122.584L122.294 66.2084L123.157 66.7129ZM101.163 79.3477V80.3477H101.163L101.163 79.3477ZM75.7063 53.8906H74.7063V53.8906L75.7063 53.8906ZM101.163 28.4336L101.163 29.4336C114.67 29.4337 125.62 40.3835 125.62 53.8906H126.62H127.62C127.62 39.2789 115.775 27.4337 101.163 27.4336L101.163 28.4336ZM126.62 53.8906L125.62 53.8906C125.62 55.5155 125.46 57.1028 125.158 58.639L126.139 58.832L127.12 59.0251C127.447 57.364 127.62 55.6474 127.62 53.8906L126.62 53.8906ZM126.139 58.832L125.273 59.332L129.823 67.2129L130.689 66.7129L131.555 66.2129L127.005 58.332L126.139 58.832ZM130.689 66.7129V65.7129H123.157V66.7129V67.7129H130.689V66.7129ZM123.157 66.7129L122.294 66.2084C118.051 73.471 110.177 78.3476 101.163 78.3477L101.163 79.3477L101.163 80.3477C110.917 80.3476 119.434 75.0681 124.021 67.2174L123.157 66.7129ZM101.163 79.3477V78.3477C87.6563 78.3477 76.7065 67.3977 76.7063 53.8906L75.7063 53.8906L74.7063 53.8906C74.7065 68.5022 86.5517 80.3477 101.163 80.3477V79.3477ZM75.7063 53.8906H76.7063C76.7063 40.3834 87.6561 29.4336 101.163 29.4336V28.4336V27.4336C86.5516 27.4336 74.7063 39.2789 74.7063 53.8906H75.7063Z" fill="white" mask="url(#path-15-outside-2_4073_1504)"/>
<defs>
<radialGradient id="paint0_radial_4073_1504" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(221 111.434) rotate(90.2836) scale(101.001 96.7134)">
<stop stop-color="white" stop-opacity="0.75"/>
<stop offset="0.620192" stop-color="white" stop-opacity="0.25"/>
<stop offset="0.956731" stop-color="white" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

@ -0,0 +1,18 @@
<svg width="226" height="201" viewBox="0 0 226 201" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M87.5166 32.9876C135.613 15.0799 185.713 30.7795 199.596 67.7217C213.479 104.664 186.03 149.237 137.934 167.145C89.8373 185.053 39.7362 169.354 25.8531 132.411C11.97 95.4692 39.42 50.8954 87.5166 32.9876Z" fill="black" stroke="white"/>
<g opacity="0.58">
<mask id="mask0_4076_766" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="21" y="25" width="183" height="151">
<path d="M87.5166 32.9876C135.613 15.0799 185.713 30.7795 199.596 67.7217C213.479 104.664 186.03 149.237 137.934 167.145C89.8373 185.053 39.7362 169.354 25.8531 132.411C11.97 95.4692 39.42 50.8954 87.5166 32.9876Z" fill="black" stroke="white"/>
</mask>
<g mask="url(#mask0_4076_766)">
<ellipse cx="91.5" cy="48" rx="101.5" ry="106" fill="url(#paint0_radial_4076_766)"/>
</g>
</g>
<defs>
<radialGradient id="paint0_radial_4076_766" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(91.5 48) rotate(90.2836) scale(101.001 96.7134)">
<stop stop-color="white" stop-opacity="0.75"/>
<stop offset="0.620192" stop-color="white" stop-opacity="0.25"/>
<stop offset="0.956731" stop-color="white" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,4 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="11" fill="black" stroke="white"/>
<path d="M1 1L8 6.5L15 1" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -0,0 +1,3 @@
<svg width="212" height="180" viewBox="0 0 212 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M81.0336 46.9085C103.283 34.1077 126.221 27.391 145.305 26.8595C164.412 26.3274 179.522 31.9898 186.349 43.7737C193.176 55.5577 190.543 71.428 180.527 87.6508C170.523 103.854 153.217 120.291 130.967 133.092C108.717 145.892 85.7786 152.61 66.6945 153.141C47.5876 153.673 32.4782 148.011 25.6507 136.227C18.8233 124.443 21.4569 108.573 31.4727 92.3497C41.4767 76.1462 58.7836 59.7094 81.0336 46.9085Z" fill="black" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 546 B

@ -0,0 +1,3 @@
<svg width="212" height="180" viewBox="0 0 212 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M81.0336 46.9565C103.283 34.1558 126.221 27.439 145.305 26.9075C164.412 26.3754 179.522 32.0379 186.349 43.8218C193.176 55.6057 190.543 71.476 180.527 87.6989C170.523 103.902 153.217 120.339 130.967 133.14C108.717 145.94 85.7786 152.658 66.6945 153.189C47.5876 153.721 32.4782 148.059 25.6507 136.275C18.8233 124.491 21.4569 108.621 31.4727 92.3977C41.4767 76.1942 58.7836 59.7574 81.0336 46.9565Z" fill="#939393" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 547 B

@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3888 0.353532L0.35791 11.3844" stroke="white"/>
<path d="M11.3844 11.3844L0.353516 0.353577" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 226 B

@ -0,0 +1,23 @@
import { useState } from 'react'
import './App.css'
import {Button, LangButton} from './comps/button'
import { useText } from './utils/usetext.jsx'
import { useNavigate } from 'react-router-dom';
function App() {
const { getText, nextLang, toggleLang } = useText();
const navigate=useNavigate();
return (
<main>
<LangButton lang={nextLang} onClick={toggleLang} />
<img src="/main.png" alt="Main" className='w-full flex-1 object-contain'/>
<Button onClick={()=>{
navigate('/keywords');
}}>{getText('button_enter')}</Button>
</main>
)
}
export default App

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -0,0 +1,23 @@
export function Button({ children, onClick, ...props }) {
return (
<button
onClick={onClick}
className={`w-full bg-black text-white border border-white uppercase text-[1.25rem] ${props.className} disabled:border-[#939393] disabled:text-[#939393] disabled:bg-transparent`}
{...props}
>
{children}
</button>
);
}
export function LangButton({ lang, onClick }) {
return (
<button
onClick={onClick}
className="bg-black text-white border border-white uppercase rounded-full text-[0.875rem] w-[1.875rem] aspect-square flex items-center justify-center absolute top-[1.5rem] right-[1.5rem]"
>
{lang}
</button>
);
}

@ -0,0 +1,225 @@
import { use, useEffect, useRef } from "react";
const StartCount=50;
const StartUpdateInterval=1000;
const PointCount=4;
const PointScale=0.7;
export default function Canvas({ lookat, ...props }) {
console.log('Canvas lookat', lookat);
const refContainer = useRef(null);
const refCanvas = useRef(null);
const refContext = useRef(null);
const refDrawing = useRef(false);
const refStars=useRef([]);
const refLastUpdate=useRef(Date.now());
const refPoints=useRef([]);
const refLookat=useRef(lookat);
function randomPointsInArc(baseangle){
const width=refCanvas.current.width;
const height=refCanvas.current.height;
const radius_x=342/393*PointScale;
const radius_y=425/393*PointScale;
const rotate_angle=54*Math.PI/180;
const angle=baseangle+(Math.random()-0.5)*Math.PI/2;
const scale=Math.random()*0.5+0.5;
let x=Math.cos(angle)*radius_x*width/2*scale;
let y=Math.sin(angle)*radius_y*width/2*scale;
// rotate
const rotated_x = x * Math.cos(rotate_angle) - y * Math.sin(rotate_angle);
const rotated_y = x * Math.sin(rotate_angle) + y * Math.cos(rotate_angle);
// translate to center
const center_x=width/2;
const center_y=height/2;
return {x: rotated_x + center_x, y: rotated_y + center_y};
}
function randomPoints(){
const points=[];
for(let i=0; i<PointCount; i++){
const start_angle = Math.random()*Math.PI*2;
const radiation_fan = Math.random()*Math.PI+Math.PI/4;
const radius = Math.random()*10+5;
const {x, y}=randomPointsInArc(Math.PI*2.0/PointCount*i);
points.push({
x,
y,
radiations: Math.floor(Math.random()*4+2),
radius: radius,
start_angle: start_angle,
radiation_fan: radiation_fan,
velocity: {x: Math.random()*.5+.5, y: Math.random()*.5+.5, angle: (Math.random()-.5)*2.0},
});
}
refPoints.current=points;
}
function movePoints(){
const width=refCanvas.current.width;
const height=refCanvas.current.height;
refPoints.current.forEach(point => {
const angle=Date.now()/1000+point.start_angle;
const radius=point.radius;
point.x+=Math.cos(angle)*radius*width/10000*point.velocity.x;
point.y+=Math.sin(angle)*radius*width/10000*point.velocity.y;
point.start_angle+=0.01*point.velocity.angle;
});
}
function randomStars(){
const stars=[];
for(let i=0; i<StartCount; i++){
stars.push({
x: Math.random(),
y: Math.random(),
alpha: Math.random(),
});
}
refStars.current=stars;
}
function draw(){
// draw random circles
const context=refContext.current;
const canvas=refCanvas.current;
context.clearRect(0, 0, canvas.width, canvas.height);
// draw random circles
refStars.current.forEach(star => {
const x = star.x * canvas.width;
const y = star.y * canvas.height;
const radius = 1;
const alpha = star.alpha;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fillStyle = `rgba(255, 255, 255, ${alpha})`;
context.fill();
});
// draw points
refPoints.current.forEach((point, index) => {
if(index===refLookat.current){
context.beginPath();
context.arc(point.x, point.y, 5, 0, Math.PI * 2);
context.fillStyle = 'rgba(255, 255, 255, 1)';
context.fill();
context.beginPath();
context.arc(point.x, point.y, 10, 0, Math.PI * 2);
context.strokeStyle = 'rgba(255, 255, 255, 1)';
context.stroke();
}
// radiations around point
for(let i=0; i<point.radiations; i++){
const angle = (i / point.radiations) * point.radiation_fan + point.start_angle;
const x = point.x + Math.cos(angle) * point.radius *(canvas.width/100);
const y = point.y + Math.sin(angle) * point.radius*(canvas.width/100);
context.beginPath();
// context.arc(x, y, 1, 0, Math.PI * 2);
// context.fillStyle = 'rgba(255, 255, 255, 0.8)';
// context.fill();
context.moveTo(point.x, point.y);
context.lineTo(x, y);
context.strokeStyle = 'rgba(255, 255, 255, 1)';
context.stroke();
}
});
// draw lines along with points
for(let i=0; i<refPoints.current.length-1; i++){
const pointA=refPoints.current[i];
const pointB=refPoints.current[i+1];
const xA = pointA.x ;
const yA = pointA.y;
const xB = pointB.x ;
const yB = pointB.y ;
context.beginPath();
context.moveTo(xA, yA);
context.lineTo(xB, yB);
context.strokeStyle = 'rgba(255, 255, 255, 1)';
context.stroke();
}
movePoints();
if(Date.now()-refLastUpdate.current>StartUpdateInterval){
randomStars();
// randomPoints();
refLastUpdate.current=Date.now();
}
refDrawing.current=requestAnimationFrame(draw);
}
useEffect(()=>{
refLookat.current=lookat;
}, [lookat]);
useEffect(()=>{
// set canvas size to container size
const container=refContainer.current;
const canvas=refCanvas.current;
canvas.width=container.clientWidth;
canvas.height=container.clientHeight;
const context=canvas.getContext('2d');
refContext.current=context;
randomStars();
randomPoints();
refDrawing.current=requestAnimationFrame(draw);
return () => {
if(refDrawing.current){
cancelAnimationFrame(refDrawing.current);
}
};
},[]);
return (
<div ref={refContainer} {...props}>
<canvas ref={refCanvas} className="w-full h-full"></canvas>
</div>
)
}

@ -0,0 +1,24 @@
@import "tailwindcss";
body{
font-family: "Space Mono", "Noto Sans TC", sans-serif;
}
.root{
@apply bg-black text-white min-h-screen flex justify-center items-center;
}
main{
@apply bg-black text-white flex justify-center items-center min-h-screen px-[1rem] py-[3.5rem] flex flex-col justify-end gap-[3rem];
}
button{
font-family: "Space Mono", "Noto Sans TC", sans-serif;
}
h1{
@apply text-[1.125rem] leading-[1.375rem] uppercase;
}
p{
@apply text-[0.75rem] leading-[1.25rem];
}

@ -0,0 +1,35 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import Keywords from './pages/keywords.jsx'
import About from './pages/about.jsx'
import Explore from './pages/explore.jsx'
import { createBrowserRouter, createHashRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import { TextProvider } from './utils/usetext.jsx'
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: '/keywords',
element: <Keywords />,
},
{
path: '/explore',
element: <Explore />,
}
]);
createRoot(document.getElementById('root')).render(
<StrictMode>
<TextProvider>
<RouterProvider router={router} />
</TextProvider>
</StrictMode>,
)

@ -0,0 +1,25 @@
import { useText } from '../utils/usetext.jsx';
export default function About() {
const { getText } = useText();
return (
<main className="z-10 fixed top-0 left-0 right-0 bg-black text-white min-h-screen flex flex-col !justify-start !items-stretch !gap-[2.63rem]">
<div className='flex flex-col gap-[1.38rem]'>
<h1>{getText('title_about')}</h1>
<p>{getText('content_about')}</p>
</div>
<div className='flex flex-col gap-[1.38rem]'>
<h1>{getText('title_credits')}</h1>
<p>{getText('content_credits')}</p>
</div>
<div className='flex flex-col gap-[1.38rem]'>
<h1>{getText('title_share')}</h1>
<span className='flex flex-row gap-[0.5rem] items-center'>
<span className='p-[0.06rem]'><img className='w-[1rem]' src="letter.svg "/></span>
<a className='underline' href="mailto:contact@ultracombos.com">contact@ultracombos.com</a>
</span>
</div>
</main>
);
}

@ -0,0 +1,92 @@
import { useEffect, useState } from "react";
import { Button } from "../comps/button";
import { useText } from "../utils/usetext.jsx";
import { useNavigate } from "react-router-dom";
import About from "./about.jsx";
import Canvas from "../comps/canvas.jsx";
const State={
Intro: 'Intro',
Explore: 'Explore',
}
export default function Explore() {
const { selectedKeyword, getText } = useText();
const navigate=useNavigate();
const [state, setState] = useState(State.Intro);
const [lookat, setLookat] = useState(selectedKeyword[0] || null);
const [showInfo, setShowInfo] = useState(false);
useEffect(()=>{
const timeoutId = setTimeout(()=>{
setState(State.Explore);
}, 2000);
return () => {
clearTimeout(timeoutId);
};
},[]);
if(state===State.Intro){
return (
<main className="!justify-center">
<div className="text-[1.125rem]">{getText('title_explore')}</div>
<img src="hint.svg"/>
</main>
);
}
return (
<>
<main className="!px-0 !gap-0 !justify-start">
<section className="absolute top-[1.5rem] left-[1.5rem] right-[1.5rem] flex flex-row justify-between items-center">
<button className="uppercase flex flex-row" onClick={()=>{
navigate('/');
}}><img src="back.svg" alt="back"/>{getText('button_reselect')}</button>
<button className="z-20 w-[1.875rem] border aspect-square flex justify-center items-center rounded-full"
onClick={()=>{
setShowInfo(!showInfo);
}}>{!showInfo?'i':<img src="x.svg" alt="close"/>}</button>
</section>
<div className="w-full relative aspect-[393/500]">
<img src="egg_bg.svg" alt="egg" className="w-full object-contain"/>
<Canvas className="absolute inset-0 top-0 left-0 w-full h-full"
lookat={selectedKeyword.indexOf(lookat)}/>
</div>
<div className="flex flex-row gap-[0.75rem] justify-center items-center mt-[1.5rem]">
<img src="eye.svg" alt="eye" className="w-[0.9965rem] object-contain"/>
<div className="text-[0.75rem]">{getText('title_observe')}</div>
</div>
<section className="w-full px-[1.75rem] flex flex-col mt-[1.44rem]">
<div className="w-full overflow-x-auto py-[1rem]">
<div className="flex flex-row gap-[0.75rem]">
{selectedKeyword.map((keyword, index) => (
<div key={index} className="w-full flex justify-center items-center border border-white"
style={{
backgroundColor: lookat && lookat.title===keyword.title ? '#939393' : 'transparent',
// color: lookat && lookat.title===keyword.title ? 'black' : 'white',
}}
onClick={()=>setLookat(keyword)}>
<span className="text-center text-nowrap text-[0.6875rem] px-[0.97rem] py-[0.47rem]">{keyword.title}</span>
</div>
))}
</div>
</div>
<div className="w-full py-[0.5rem]">
{lookat && (
<div className="text-[0.875rem] tracking-[0.0875rem] leading-[160%]">{lookat.content}</div>
)}
</div>
</section>
</main>
{showInfo && <About/>}
</>
);
}

@ -0,0 +1,62 @@
import { useEffect, useRef, useState } from 'react'
import {Button} from "../comps/button";
import { useText } from "../utils/usetext.jsx";
import { useNavigate } from "react-router-dom";
export default function Keywords() {
const { getText, getKeywords, selectedKeyword, select } = useText();
const keywords = getKeywords();
const rowCount=3;
const colCount = Math.ceil(keywords.length / rowCount);
const refContainer=useRef();
const navigate=useNavigate();
useEffect(()=>{
const container=refContainer.current;
// scroll to center
container.scrollLeft=(container.scrollWidth-container.clientWidth)/2;
return () => {
};
}, [keywords]);
return (
<main className="!px-0 !justify-between">
<h1 className="text-[1.125rem] mt-[3.875rem]">{getText('title_keywords')}</h1>
<section ref={refContainer} className="w-full h-[107vw] overflow-x-auto overflow-y-hidden relative">
{keywords.map((keyword, index) => (
<div key={index} className="absolute w-[54vw] aspect-[212/180] bg-contain bg-no-repeat flex items-center justify-center"
style={{
top: `${Math.floor(index/colCount) * 32}vw`,
left: `${(index % colCount) * 40 + 5*Math.floor(index/colCount)}vw`,
backgroundImage: `url(/topic${selectedKeyword.includes(keyword) ? '_select' : ''}.svg)`,
}}
onClick={()=>{
console.log('clicked keyword', keyword.title);
select(keyword);
}}>
<span className="break-word max-w-[50%] text-center">{keyword.title}</span>
</div>
))}
</section>
<div className="w-full px-[1.5rem]">
<Button onClick={()=>{
navigate('/explore');
}}
disabled={selectedKeyword.length<4}
>
{getText('button_explore')}
</Button>
</div>
</main>
);
}

@ -0,0 +1,67 @@
import { createContext, useContext, useEffect, useState } from "react";
import useSWR from "swr";
const TextContent=createContext();
const LANG={
"en":"EN",
"zh":"中"
};
export function TextProvider({children}) {
const { data }=useSWR("/data.json", async (url)=>{
const res=await fetch(url);
return res.json();
});
const [lang, setLang]=useState("en");
const [nextLang, setNextLang]=useState("zh");
const [selectedKeyword, setSelectedKeyword] = useState([]);
function getText(key) {
// console.log("getText", key, lang, data?.[key]?.[lang]);
return data?.[key]?.[lang] || "";
}
function toggleLang() {
const currentIndex=Object.keys(LANG).indexOf(lang);
const nextIndex=(currentIndex+1)%Object.keys(LANG).length;
setLang(Object.keys(LANG)[nextIndex]);
setNextLang(Object.values(LANG)[currentIndex]);
}
function getKeywords(){
return data?.keywords?.[lang] || [];
}
function select(select){
if(selectedKeyword.includes(select)){
setSelectedKeyword(selectedKeyword.filter(item=>item!==select));
}else{
if(selectedKeyword.length>=4){
// remove the first one, insert the new one at the end
setSelectedKeyword([...selectedKeyword.slice(1), select]);
}else{
setSelectedKeyword([...selectedKeyword, select]);
}
}
}
useEffect(()=>{
console.log("selectedKeyword changed", selectedKeyword);
}, [selectedKeyword]);
return (
<TextContent.Provider value={{ data, getText, nextLang, toggleLang, getKeywords, selectedKeyword, select }}>
{children}
</TextContent.Provider>
)
}
export function useText() {
return useContext(TextContent);
}

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})
Loading…
Cancel
Save