├── public ├── .nojekyll ├── genkotsu.png ├── gif.worker.js └── gif.worker.js.map ├── genkotsu.png ├── src ├── assets │ └── keifont.ttf ├── styles │ ├── localFonts.ts │ └── globalStyle.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx ├── components │ └── GenkotsuDrawer.tsx └── libs │ └── draw.ts ├── README.md ├── tsconfig.json ├── package.json ├── next.config.js ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── 【源真ゴシック・源ノ角ゴシック】Apache License 2.0.txt └── yarn.lock /public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /genkotsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inaniwaudon/genkotsu/HEAD/genkotsu.png -------------------------------------------------------------------------------- /public/genkotsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inaniwaudon/genkotsu/HEAD/public/genkotsu.png -------------------------------------------------------------------------------- /src/assets/keifont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inaniwaudon/genkotsu/HEAD/src/assets/keifont.ttf -------------------------------------------------------------------------------- /src/styles/localFonts.ts: -------------------------------------------------------------------------------- 1 | import localFont from 'next/font/local' 2 | 3 | export const keiFont = localFont({ 4 | src: '../assets/keifont.ttf', 5 | display: 'swap', 6 | }) -------------------------------------------------------------------------------- /src/styles/globalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components' 2 | import { keiFont } from './localFonts' 3 | 4 | export const GlobalStyle = createGlobalStyle` 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | :root { 12 | --kei-font: ${keiFont.style.fontFamily}; 13 | } 14 | ` 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # genkotsu 2 | 3 | ![げんこつ](./genkotsu.png) 4 | 5 | https://inaniwaudon.github.io/genkotsu/ 6 | 7 | ```bash 8 | yarn 9 | yarn run dev # launch on local env 10 | yarn run build # build 11 | ``` 12 | 13 | 表示用に「[けいふぉんと](http://font.sumomo.ne.jp/font_1.html)」を使用しています。The keifont, that is included in this site, is distributed in the Apache License 2.0. 14 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalStyle } from '@/styles/globalStyle' 2 | import type { AppProps } from 'next/app' 3 | import React from 'react' 4 | 5 | export default function App({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genkotsu", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next export", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "gif.js": "^0.2.0", 14 | "next": "13.3.2", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "styled-components": "^5.3.10" 18 | }, 19 | "devDependencies": { 20 | "@types/gif.js": "^0.2.2", 21 | "@types/node": "18.16.3", 22 | "@types/react": "18.2.0", 23 | "@types/react-dom": "18.2.1", 24 | "@types/styled-components": "^5.1.26", 25 | "copy-webpack-plugin": "^11.0.0", 26 | "eslint": "8.39.0", 27 | "eslint-config-next": "^13.3.4", 28 | "typescript": "5.0.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | basePath: process.env.GITHUB_ACTIONS ? "/genkotsu" : "", 8 | webpack: (config) => { 9 | const nextPublicDirPath = path.resolve(__dirname, "public"); 10 | config.plugins.push( 11 | new CopyPlugin({ 12 | patterns: [ 13 | { 14 | from: "./node_modules/gif.js/dist/gif.worker.js", 15 | to: nextPublicDirPath, 16 | }, 17 | { 18 | from: "./node_modules/gif.js/dist/gif.worker.js.map", 19 | to: nextPublicDirPath, 20 | }, 21 | ], 22 | }) 23 | ); 24 | 25 | return config; 26 | }, 27 | trailingSlash: true, 28 | }; 29 | 30 | module.exports = nextConfig; 31 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | DocumentContext, 3 | Head, 4 | Html, 5 | Main, 6 | NextScript, 7 | } from "next/document"; 8 | import { ServerStyleSheet } from "styled-components"; 9 | 10 | export default class MyDocument extends Document { 11 | static async getInitialProps(ctx: DocumentContext) { 12 | const sheet = new ServerStyleSheet(); 13 | const originalRenderPage = ctx.renderPage; 14 | 15 | try { 16 | ctx.renderPage = () => 17 | originalRenderPage({ 18 | enhanceApp: (App) => (props) => 19 | sheet.collectStyles(), 20 | }); 21 | 22 | const initialProps = await Document.getInitialProps(ctx); 23 | return { 24 | ...initialProps, 25 | styles: ( 26 | <> 27 | {initialProps.styles} 28 | {sheet.getStyleElement()} 29 | 30 | ), 31 | }; 32 | } finally { 33 | sheet.seal(); 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v5 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v6 20 | with: 21 | node-version: "18" 22 | 23 | - name: Get yarn cache 24 | id: yarn-cache 25 | run: echo "::set-output name=dir::$(yarn cache dir)" 26 | 27 | - name: Cache dependencies 28 | uses: actions/cache@v4 29 | with: 30 | path: ${{ steps.yarn-cache.outputs.dir }} 31 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-yarn- 34 | 35 | - name: Install dependencies 36 | run: yarn install --frozen-lockfile 37 | - name: Build 38 | run: yarn build 39 | - name: Export 40 | run: yarn export 41 | - name: Upload 42 | uses: actions/upload-pages-artifact@v4 43 | with: 44 | path: out 45 | 46 | deploy: 47 | needs: build 48 | permissions: 49 | pages: write 50 | id-token: write 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Deploy 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,nextjs,react 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,nextjs,react 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### NextJS ### 38 | # dependencies 39 | /node_modules 40 | /.pnp 41 | .pnp.js 42 | 43 | # testing 44 | /coverage 45 | 46 | # next.js 47 | /.next/ 48 | /out/ 49 | 50 | # production 51 | /build 52 | 53 | # misc 54 | *.pem 55 | 56 | # debug 57 | npm-debug.log* 58 | yarn-debug.log* 59 | yarn-error.log* 60 | .pnpm-debug.log* 61 | 62 | # local env files 63 | .env*.local 64 | 65 | # vercel 66 | .vercel 67 | 68 | # typescript 69 | *.tsbuildinfo 70 | next-env.d.ts 71 | 72 | ### react ### 73 | .DS_* 74 | *.log 75 | logs 76 | **/*.backup.* 77 | **/*.back.* 78 | 79 | node_modules 80 | bower_components 81 | 82 | *.sublime* 83 | 84 | psd 85 | thumb 86 | sketch 87 | 88 | ### VisualStudioCode ### 89 | .vscode/* 90 | !.vscode/settings.json 91 | !.vscode/tasks.json 92 | !.vscode/launch.json 93 | !.vscode/extensions.json 94 | !.vscode/*.code-snippets 95 | 96 | # Local History for Visual Studio Code 97 | .history/ 98 | 99 | # Built Visual Studio Code Extensions 100 | *.vsix 101 | 102 | ### VisualStudioCode Patch ### 103 | # Ignore all local history of files 104 | .history 105 | .ionide 106 | 107 | # End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,nextjs,react -------------------------------------------------------------------------------- /src/components/GenkotsuDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useTransition } from "react"; 2 | import styled from "styled-components"; 3 | import { createGif, drawGenkotsu, drawerHeight, drawerWidth } from "@/libs/draw"; 4 | 5 | const maxWidth = 400 * (16 / 9); 6 | 7 | const Wrapper = styled.section` 8 | width: 100%; 9 | max-width: ${maxWidth}px; 10 | `; 11 | 12 | const Canvas = styled.canvas` 13 | height: 400px; 14 | 15 | @media screen and (max-width: ${maxWidth + 32 * 2}px) { 16 | width: 100%; 17 | height: auto; 18 | } 19 | `; 20 | 21 | const Navigation = styled.div` 22 | display: flex; 23 | 24 | @media screen and (max-width: ${maxWidth + 32 * 2}px) { 25 | flex-direction: column; 26 | gap: 8px; 27 | } 28 | `; 29 | 30 | const Download = styled.a` 31 | width: 100%; 32 | max-width: 300px; 33 | text-align: center; 34 | margin: 16px auto 0 auto; 35 | padding: 8px 0; 36 | border-radius: 4px; 37 | cursor: pointer; 38 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); 39 | display: block; 40 | `; 41 | 42 | interface GenkotsuDrawerProps { 43 | text: string; 44 | } 45 | 46 | const GenkotsuDrawer = ({ text }: GenkotsuDrawerProps) => { 47 | const [_, startTransition] = useTransition(); 48 | 49 | const canvasRef = useRef(null); 50 | 51 | useEffect(() => { 52 | startTransition(() => { 53 | if (canvasRef.current) { 54 | const context = canvasRef.current.getContext("2d"); 55 | if (!context) { 56 | return; 57 | } 58 | document.fonts.ready.then(() => 59 | drawGenkotsu(text, drawerWidth, drawerHeight, context) 60 | ) 61 | } 62 | }); 63 | }, [text]); 64 | 65 | const downloadImage = () => { 66 | if (canvasRef.current) { 67 | const link = document.createElement("a"); 68 | link.href = canvasRef.current.toDataURL("image/png"); 69 | link.download = "genkotsu.png"; 70 | link.click(); 71 | } 72 | }; 73 | 74 | const downloadGif = () => { 75 | createGif(text); 76 | }; 77 | 78 | return ( 79 | 80 | 81 | 82 | 画像をダウンロード 83 | GIF 画像をダウンロード 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default GenkotsuDrawer; 90 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Script from "next/script"; 3 | import Head from "next/head"; 4 | import styled from "styled-components"; 5 | import GenkotsuDrawer from "@/components/GenkotsuDrawer"; 6 | 7 | const Main = styled.main` 8 | font-family: sans-serif; 9 | margin: 32px; 10 | `; 11 | 12 | const Input = styled.input` 13 | max-width: 100%; 14 | font-size: 48px; 15 | font-family: var(--kei-font); 16 | margin-bottom: 24px; 17 | border-top: none; 18 | border-right: none; 19 | border-left: none; 20 | border-bottom: solid 1px #ccc; 21 | `; 22 | 23 | const Footer = styled.footer` 24 | margin-top: 16px; 25 | `; 26 | 27 | const Anchor = styled.a` 28 | color: #666; 29 | text-underline-offset: 4px; 30 | `; 31 | 32 | const SnsParagraph = styled.div` 33 | display: flex; 34 | align-items: center; 35 | gap: 16px; 36 | `; 37 | 38 | const Index = () => { 39 | const [text, setText] = useState("げんこつ"); 40 | const [isClient, setIsClient] = useState(false); 41 | useEffect(() => { 42 | setIsClient(true); 43 | }, []); 44 | 45 | return ( 46 | <> 47 | 48 | げんこつ 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 63 | 64 | 68 | 69 |
70 |
71 | setText(e.currentTarget.value)} 75 | /> 76 |
77 | 78 |