diff --git a/daemon/Makefile b/daemon/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/frontend/app/(pages)/blog/BlogPostCard.tsx b/frontend/app/(pages)/blog/BlogPostCard.tsx index 9e9449d..1d49e9e 100644 --- a/frontend/app/(pages)/blog/BlogPostCard.tsx +++ b/frontend/app/(pages)/blog/BlogPostCard.tsx @@ -24,7 +24,7 @@ type Props = { function BlogPostCard({ post }: Props) { return (
- { try { const postListResponse = await fetchPostList(page); const posts = postListResponse.data.posts; + return ( // blog list
-
    - { posts.map((post, key: number) => ( -
  • - - - -
  • ) - ) } -
- +
); } catch (e: any) { diff --git a/frontend/app/(pages)/blog/post/[id]/MinifyPostCard.tsx b/frontend/app/(pages)/blog/post/[id]/MinifyPostCard.tsx index 4352659..8ec3c9b 100644 --- a/frontend/app/(pages)/blog/post/[id]/MinifyPostCard.tsx +++ b/frontend/app/(pages)/blog/post/[id]/MinifyPostCard.tsx @@ -17,7 +17,7 @@ const MinifyPostCard = (props: Props) => { className="w-full h-[300px] shadow-none border-0 rounded-none overflow-hidden" as={ Link } isPressable - href={ `/blog/post/${ post['id'] }` } + href={ `/blog/post/${ post['identifier'] }` } >

{ desc }

diff --git a/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx b/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx new file mode 100644 index 0000000..b5dd424 --- /dev/null +++ b/frontend/app/(pages)/blog/post/components/AnimationPostList.tsx @@ -0,0 +1,31 @@ +'use client'; + +import React from 'react'; +import BlogPostCard from '@/app/(pages)/blog/BlogPostCard'; + +import * as motion from 'motion/react-client'; +import { AnimatePresence } from 'motion/react'; +import { Post } from '@/src/api/blog/post'; + +const PostList = ({ posts }: { posts: Post[] }) => { + return ( +
+
    + + { posts.map((post, key: number) => ( + + + ) + ) } + +
+ +
+ ); +}; +export default PostList; diff --git a/frontend/app/globals.css b/frontend/app/globals.css index b9df942..b952fe6 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,4 +1,4 @@ -@import "lxgw-wenkai-webfont/style.css"; +@import "lxgw-wenkai-webfont/lxgwwenkai-light.css"; @tailwind base; @tailwind components; @@ -6,8 +6,10 @@ body { font-family: "LXGW WenKai", sans-serif; + /*font-family: "Arial", "monospace", sans-serif;*/ } pre, code { - font-family: "LXGW WenKai Mono", sans-serif; -} + /*font-family: "monospace", sans-serif;*/ + font-family: "monospace", sans-serif; +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 91e4bf3..fa2b5de 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,14 +5,19 @@ import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger'; import { Button } from '@nextui-org/react'; import { entries } from '@/app/entries'; import Link from 'next/link'; +import BoxesGL from '@/components/BackgroundBoxGL'; +import { BoxesCore } from '@/components/BackgroundBox'; export default function Home() { const isMobile = useScreenSizeTrigger('sm'); return ( -
-
+
+ {/**/ } + + +

Atelier Lumiaethor

@@ -21,7 +26,8 @@ export default function Home() { className={ 'absolute bottom-0 w-full h-60 p-12' } >
+ // className={ 'bg-blue-500 h-full w-full rounded-3xl backdrop-blur align-middle grid grid-cols-2 content-center' }> + className={ 'h-full w-full rounded-3xl backdrop-blur align-middle grid grid-cols-2 content-center' }>
diff --git a/frontend/components/BackgroundBox.tsx b/frontend/components/BackgroundBox.tsx new file mode 100644 index 0000000..295074f --- /dev/null +++ b/frontend/components/BackgroundBox.tsx @@ -0,0 +1,76 @@ +'use client'; +import React from 'react'; +import { motion } from 'motion/react'; +import { cn } from '@/src/utils'; + +export const BoxesCore = ({ className, ...rest }: { className?: string }) => { + const rows = new Array(250).fill(1); + const cols = new Array(200).fill(1); + let colors = [ + '#93c5fd', + '#f9a8d4', + '#86efac', + '#fde047', + '#fca5a5', + '#d8b4fe', + '#93c5fd', + '#a5b4fc', + '#c4b5fd' + ]; + const getRandomColor = () => { + return colors[Math.floor(Math.random() * colors.length)]; + }; + + return ( +
+ { rows.map((_, i) => ( + + { cols.map((_, j) => ( + + { j % 2 === 0 && i % 2 === 0 ? ( + + + + ) : null } + + )) } + + )) } +
+ ); +}; + +export const Boxes = React.memo(BoxesCore); diff --git a/frontend/components/BackgroundBoxGL.tsx b/frontend/components/BackgroundBoxGL.tsx new file mode 100644 index 0000000..7f26ed0 --- /dev/null +++ b/frontend/components/BackgroundBoxGL.tsx @@ -0,0 +1,266 @@ +'use client'; +import React, { useEffect, useRef } from 'react'; + +// WebGL-based rewrite of the grid/hover effect originally implemented with many DOM nodes. +// Key design goals: +// - zero per-cell DOM; a single full-screen draw with a fragment shader +// - hover color derived deterministically from a small palette +// - optional CSS transform to match the original perspective/skew +// - high-DPI aware; resizes with container + +export default function BoxesGL({ + className, + style, + rows = 250, + cols = 200, + lineWidth = 1.0, // device pixels + transformLikeOriginal = true +}: { + className?: string + style?: React.CSSProperties + rows?: number + cols?: number + lineWidth?: number + transformLikeOriginal?: boolean +}) { + const canvasRef = useRef(null); + const rafRef = useRef(null); + const mouseRef = useRef<{ x: number; y: number } | null>(null); + + useEffect(() => { + const canvas = canvasRef.current!; + const gl = canvas.getContext('webgl2', { antialias: false, premultipliedAlpha: true }); + if (!gl) { + console.warn('WebGL2 not available'); + // Fail closed; nothing to do if WebGL2 is unavailable + return; + } + + // Create program (fullscreen triangle) + const vs = gl.createShader(gl.VERTEX_SHADER)!; + gl.shaderSource( + vs, + `#version 300 es\n + const vec2 verts[3] = vec2[3]( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0) + ); + void main(){ + gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); + } + ` + ); + gl.compileShader(vs); + + const fs = gl.createShader(gl.FRAGMENT_SHADER)!; + gl.shaderSource( + fs, + `#version 300 es\n + precision highp float; + out vec4 o; + + uniform vec2 u_resolution; // canvas size in device pixels + uniform vec2 u_mouse; // mouse in device pixels, (-1,-1) if outside + uniform float u_time; // seconds + uniform float u_rows; + uniform float u_cols; + uniform float u_lineWidth; // in device pixels + + // Border color and plus sign color (matches tailwind slate-700-ish) + const vec3 BORDER = vec3(0.22, 0.27, 0.33); + const vec3 PLUS = vec3(0.22, 0.27, 0.33); + + // A small pastel palette like the original + vec3 palette(int idx){ + // 9-color palette + if(idx==0) return vec3(0.576,0.773,0.992); + if(idx==1) return vec3(0.976,0.659,0.831); + if(idx==2) return vec3(0.525,0.937,0.675); + if(idx==3) return vec3(0.992,0.878,0.278); + if(idx==4) return vec3(0.988,0.648,0.648); + if(idx==5) return vec3(0.847,0.706,0.996); + if(idx==6) return vec3(0.576,0.773,0.992); + if(idx==7) return vec3(0.647,0.706,0.988); + return vec3(0.769,0.710,0.992); + } + + // Hash helpers for stable per-cell randomness + float hash11(float x){ + x = fract(x * 0.1031); + x *= x + 33.33; + return fract(1e4 * x * (x + 0.33)); + } + float hash21(vec2 p){ + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + + void main(){ + vec2 frag = gl_FragCoord.xy; // device pixels + vec2 gridSize = vec2(u_cols, u_rows); + vec2 cellSize = u_resolution / gridSize; // pixels per cell + + // Compute integer cell coordinates and local position within cell [0,1) + vec2 uvGrid = frag / u_resolution * gridSize; + vec2 cellId = floor(uvGrid); + vec2 cellUV = fract(uvGrid); + + // Grid lines: draw top+right borders to mimic original (border-t, border-r, border-l on first col) + float lw = u_lineWidth / max(cellSize.x, 1.0); // convert to relative width in [0,1] axis + float lwY = u_lineWidth / max(cellSize.y, 1.0); + + float lineTop = step(1.0 - lwY, cellUV.y); + float lineRight= step(1.0 - lw , cellUV.x); + float lineLeft = step(cellUV.x, lw) * step(0.5, step(0.0, cellId.x)); // left border for all cols via small bias + + float lineMask = max(lineTop, max(lineRight, lineLeft)); + + // Determine hovered cell index + vec2 hoverCell = vec2(-1.0); + bool hovering = u_mouse.x >= 0.0 && u_mouse.y >= 0.0; + if(hovering){ + vec2 hoverUVGrid = u_mouse / u_resolution * gridSize; + hoverCell = floor(hoverUVGrid); + } + bool isHover = hovering && all(equal(hoverCell, cellId)); + + // Background: transparent unless hovered. On hover, fill with palette color chosen by cell hash. + vec3 bg = vec3(0.0); + float alpha = 0.0; + if(isHover){ + // Stable color per cell, with slight time wobble to avoid banding on long hovers + float h = hash21(cellId + vec2(floor(u_time*0.25))); + int idx = int(floor(h * 9.0)) % 9; + bg = palette(idx); + alpha = 1.0; + } + + // Plus sign for even-even cells: thin cross centered + bool evenEven = mod(cellId.x, 2.0) < 0.5 && mod(cellId.y, 2.0) < 0.5; + float plusThicknessX = 0.5 * u_lineWidth / max(cellSize.x, 1.0); + float plusThicknessY = 0.5 * u_lineWidth / max(cellSize.y, 1.0); + float cross = 0.0; + if(evenEven){ + float vert = step(0.5 - plusThicknessX, abs(cellUV.x - 0.5)); + float hori = step(0.5 - plusThicknessY, abs(cellUV.y - 0.5)); + cross = 1.0 - max(vert, hori); + } + + vec3 color = mix(bg, BORDER, max(lineMask, 0.0)); + color = mix(color, PLUS, cross); + float a = max(alpha, max(lineMask, cross)); + + o = vec4(color, a); + } + ` + ); + gl.compileShader(fs); + + const prog = gl.createProgram()!; + gl.attachShader(prog, vs); + gl.attachShader(prog, fs); + gl.linkProgram(prog); + + // Check compile/link for diagnostics (silent in prod; could surface to console in dev) + if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { + console.warn('WebGL program link error:', gl.getProgramInfoLog(prog)); + } + + gl.useProgram(prog); + + const uResolution = gl.getUniformLocation(prog, 'u_resolution'); + const uMouse = gl.getUniformLocation(prog, 'u_mouse'); + const uTime = gl.getUniformLocation(prog, 'u_time'); + const uRows = gl.getUniformLocation(prog, 'u_rows'); + const uCols = gl.getUniformLocation(prog, 'u_cols'); + const uLineWidth = gl.getUniformLocation(prog, 'u_lineWidth'); + + // Single draw call state + gl.disable(gl.DEPTH_TEST); + gl.disable(gl.CULL_FACE); + gl.viewport(0, 0, canvas.width, canvas.height); + + // Resize handling (devicePixelRatio-aware) + const resize = () => { + const dpr = Math.max(window.devicePixelRatio || 1, 1); + const { clientWidth, clientHeight } = canvas; + const w = Math.max(1, Math.floor(clientWidth * dpr)); + const h = Math.max(1, Math.floor(clientHeight * dpr)); + if (canvas.width !== w || canvas.height !== h) { + canvas.width = w; + canvas.height = h; + gl.viewport(0, 0, w, h); + } + }; + + const observer = new ResizeObserver(resize); + observer.observe(canvas); + resize(); + + // Mouse tracking in device pixels + const onMove = (e: PointerEvent) => { + const rect = canvas.getBoundingClientRect(); + const dpr = Math.max(window.devicePixelRatio || 1, 1); + const x = (e.clientX - rect.left) * dpr; + const y = (rect.bottom - e.clientY) * dpr; // flip Y to GL coords + mouseRef.current = { x, y }; + }; + const onLeave = () => { + mouseRef.current = null; + }; + canvas.addEventListener('pointermove', onMove); + canvas.addEventListener('pointerleave', onLeave); + + const t0 = performance.now(); + const tick = () => { + const t = (performance.now() - t0) / 1000; + gl.useProgram(prog); + gl.uniform2f(uResolution, canvas.width, canvas.height); + const m = mouseRef.current; + if (m) gl.uniform2f(uMouse, m.x, m.y); + else gl.uniform2f(uMouse, -1, -1); + gl.uniform1f(uTime, t); + gl.uniform1f(uRows, rows); + gl.uniform1f(uCols, cols); + gl.uniform1f(uLineWidth, lineWidth); + + gl.drawArrays(gl.TRIANGLES, 0, 3); + rafRef.current = requestAnimationFrame(tick); + }; + tick(); + + return () => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + observer.disconnect(); + canvas.removeEventListener('pointermove', onMove); + canvas.removeEventListener('pointerleave', onLeave); + gl.deleteProgram(prog); + gl.deleteShader(vs); + gl.deleteShader(fs); + }; + }, [ rows, cols, lineWidth ]); + + return ( +
+ +
+ ); +} diff --git a/frontend/package.json b/frontend/package.json index a88597e..7fc5928 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,8 +11,9 @@ "dependencies": { "@nextui-org/react": "^2.4.6", "@types/react-syntax-highlighter": "^15.5.13", + "clsx": "^2.1.1", "lxgw-wenkai-webfont": "^1.7.0", - "motion": "^12.3.1", + "motion": "^12.23.12", "next": "^15.3.2", "next-themes": "^0.3.0", "react": "^18.3.1", @@ -26,7 +27,9 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-toc": "^9.0.0", - "sharp": "^0.33.4" + "sharp": "^0.33.4", + "tailwind-merge": "^3.3.1", + "ts-pattern": "^5.8.0" }, "devDependencies": { "@tailwindcss/typography": "^0.5.14", diff --git a/frontend/src/api/blog/post.ts b/frontend/src/api/blog/post.ts index fc3f0b9..5dedce7 100644 --- a/frontend/src/api/blog/post.ts +++ b/frontend/src/api/blog/post.ts @@ -1,20 +1,22 @@ import { fetchNocturneJson } from '@/src/api/endpoint'; -const DEFAULT_PAGE_SIZE = 6; +const DEFAULT_PAGE_SIZE = 8; + +export interface Post { + id: string, + identifier: string, + title: string, + subtitle: string, + tags: string[] + category: string, + date_created: Date, + date_updated: Date | null, + header_img: string | null +} export interface PostListResponse { data: { - posts: { - id: string, - identifier: string, - title: string, - subtitle: string, - tags: string[] - category: string, - date_created: Date, - date_updated: Date | null, - header_img: string | null - }[], + posts: Post[], pagination: { page: number, page_count: number, diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts new file mode 100644 index 0000000..883213c --- /dev/null +++ b/frontend/src/utils.ts @@ -0,0 +1,6 @@ +import { ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +}