@bbki.ng/site 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +44 -0
- package/.husky/pre-commit +4 -0
- package/.prettierignore +1 -0
- package/.prettierrc.json +1 -0
- package/.rush/temp/shrinkwrap-deps.json +908 -0
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +11 -0
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/index.html +33 -0
- package/jest.config.js +15 -0
- package/package.json +69 -0
- package/postcss.config.cjs +6 -0
- package/public/Logo.svg +9 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/robots.txt +2 -0
- package/src/__test__/utils/index.test.ts +90 -0
- package/src/app.tsx +97 -0
- package/src/articles/anti-logic.mdx +61 -0
- package/src/articles/bbking-manual.mdx +88 -0
- package/src/articles/black-river.mdx +8 -0
- package/src/articles/cooldown.mdx +12 -0
- package/src/articles/fall.mdx +8 -0
- package/src/articles/img.mdx +104 -0
- package/src/articles/index.ts +35 -0
- package/src/articles/loading.mdx +129 -0
- package/src/articles/major-cold.mdx +14 -0
- package/src/articles/men-without-women.mdx +19 -0
- package/src/articles/movie-day.mdx +15 -0
- package/src/articles/now.mdx +15 -0
- package/src/articles/projects.mdx +9 -0
- package/src/articles/quote.mdx +27 -0
- package/src/articles/spring-cooldown.mdx +7 -0
- package/src/articles/spring-rain.mdx +9 -0
- package/src/articles/travel.mdx +21 -0
- package/src/articles/warming-up.mdx +10 -0
- package/src/articles/web-burnning.mdx +10 -0
- package/src/auth_required.tsx +17 -0
- package/src/components/Spinner.tsx +33 -0
- package/src/components/article/index.tsx +31 -0
- package/src/components/aspect_ratio_box/index.tsx +29 -0
- package/src/components/blur_cover/index.tsx +28 -0
- package/src/components/book_list/index.tsx +50 -0
- package/src/components/comment/index.tsx +70 -0
- package/src/components/comment/use_cusdis_event.ts +37 -0
- package/src/components/corner_prompt_box/index.tsx +63 -0
- package/src/components/disabled_text/index.tsx +23 -0
- package/src/components/fade_out_cover/index.tsx +37 -0
- package/src/components/footer/footer_links.ts +13 -0
- package/src/components/footer/index.tsx +21 -0
- package/src/components/hotkey_nav/index.tsx +50 -0
- package/src/components/img_list/index.tsx +43 -0
- package/src/components/index.tsx +27 -0
- package/src/components/movie_list/index.tsx +50 -0
- package/src/components/my_suspense.tsx +14 -0
- package/src/components/progress_bar/index.tsx +31 -0
- package/src/components/reload_prompt/index.tsx +36 -0
- package/src/components/stickers/index.tsx +46 -0
- package/src/components/table_skeleton/index.tsx +40 -0
- package/src/components/tags/index.tsx +52 -0
- package/src/components/video_player/index.tsx +81 -0
- package/src/components/with_wrapper/index.tsx +13 -0
- package/src/constants/cusdis.ts +6 -0
- package/src/constants/index.ts +16 -0
- package/src/constants/photo_projects.ts +54 -0
- package/src/constants/photos.ts +270 -0
- package/src/constants/routes.ts +24 -0
- package/src/constants/video_logs.ts +16 -0
- package/src/demo/DemoBox.tsx +15 -0
- package/src/demo/ImgDemo.tsx +34 -0
- package/src/demo/SpinnerDemo.tsx +17 -0
- package/src/global/mdx.d.ts +8 -0
- package/src/global_loading_state_provider.tsx +27 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useScrollToTop.ts +24 -0
- package/src/hooks/useTransitionCls.ts +36 -0
- package/src/hooks/use_img_loading.ts +16 -0
- package/src/hooks/use_pathname.ts +6 -0
- package/src/hooks/use_paths.ts +30 -0
- package/src/hooks/use_projects.ts +56 -0
- package/src/hooks/use_route_name.ts +7 -0
- package/src/hooks/use_supa_session.ts +30 -0
- package/src/hooks/use_uploader.ts +34 -0
- package/src/hooks/use_video_controls.ts +71 -0
- package/src/main.css +156 -0
- package/src/main.tsx +19 -0
- package/src/pages/cover/index.tsx +10 -0
- package/src/pages/extensions/png/consts.ts +9 -0
- package/src/pages/extensions/png/index.tsx +41 -0
- package/src/pages/extensions/png/png_projects.tsx +63 -0
- package/src/pages/extensions/txt/article.tsx +26 -0
- package/src/pages/extensions/txt/consts.ts +8 -0
- package/src/pages/extensions/txt/index.tsx +21 -0
- package/src/pages/index.tsx +14 -0
- package/src/pages/login/index.tsx +33 -0
- package/src/pages/now/index.tsx +7 -0
- package/src/pages/tags/index.tsx +28 -0
- package/src/pages/tags/tag_result.tsx +19 -0
- package/src/swr.tsx +18 -0
- package/src/types/articles.ts +6 -0
- package/src/types/color.ts +21 -0
- package/src/types/cusdis.ts +4 -0
- package/src/types/oss.ts +15 -0
- package/src/types/path.ts +11 -0
- package/src/types/photo.ts +17 -0
- package/src/types/supabase.ts +9 -0
- package/src/utils/index.ts +143 -0
- package/src/utils/tags.ts +21 -0
- package/tailwind.config.cjs +10 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +108 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 图片展示
|
|
3
|
+
tags:
|
|
4
|
+
- bbki.ng
|
|
5
|
+
- web
|
|
6
|
+
- 图片
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
import { ImgDemo } from "@/demo/ImgDemo";
|
|
10
|
+
|
|
11
|
+
图片是 BBKi.ng 的主要内容之一,友好地展示图片是必须考虑的问题。
|
|
12
|
+
|
|
13
|
+
先写下第一行代码:
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<img src="my-awesome-picture.jpg" />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
这样就完成了图片展示。但是这种极简的做法存在一些问题,还谈不上友好。比如:
|
|
20
|
+
|
|
21
|
+
1. 布局偏移问题。我们没有为 `img` 标签设定尺寸,导致图片加载期间,浏览器没有在文档中为其分配正确的空间。加载完成后,图片突然撑开。
|
|
22
|
+
2. 性能问题。没有惰性渲染、惰性加载,多图页面会有潜在性能问题。
|
|
23
|
+
3. 逐行渲染问题。标准 JPEG 格式压缩的图片,下载时基线 JPEG 算法会逐行渲染图片,影响体验。
|
|
24
|
+
|
|
25
|
+
## 布局偏移
|
|
26
|
+
|
|
27
|
+
对于问题 1. 我们为图片设置好宽高即可:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<style>
|
|
31
|
+
img {
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
height: auto;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
<img height="853" width="1280" … />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 性能相关
|
|
40
|
+
|
|
41
|
+
### `content-visibility: auto`
|
|
42
|
+
|
|
43
|
+
该 CSS 属性会告知浏览器,在图片接近屏幕前,先不要为其布局。浏览器也不会去解码用户暂时看不见的图片,节省 CPU。
|
|
44
|
+
|
|
45
|
+
### `loading="lazy"`
|
|
46
|
+
|
|
47
|
+
该属性会告知浏览器,在图片接近屏幕前,先不要去请求图片资源。
|
|
48
|
+
|
|
49
|
+
### `decoding="async"`
|
|
50
|
+
|
|
51
|
+
该属性会给浏览器脱离主线程解码图片的权限,避免用来解码图片的 CPU 时间影响用户。
|
|
52
|
+
|
|
53
|
+
## 呈现相关
|
|
54
|
+
|
|
55
|
+
### 渐进加载
|
|
56
|
+
|
|
57
|
+
直观体验:
|
|
58
|
+
|
|
59
|
+
- [Medium](https://medium.com/cucumbertown-magazine/the-beginners-guide-to-composition-in-food-photography-how-to-transform-your-food-photos-from-good-39613ab78bf2)
|
|
60
|
+
- [Polymer shop project](https://shop.polymer-project.org/)
|
|
61
|
+
|
|
62
|
+
上边两个例子在加载大图时,都用到了低质量图片占位(Low Quality Image Placeholder, LQIP)。在大图加载时,展示模糊的低质量图片,在加载低质量图片时,展示背景色。图片加载成功时,配合 transition 效果,整个过程显得顺滑流畅。
|
|
63
|
+
[José M. Pérez](https://jmperezperez.com/about-me/) 在 2015 年写了[一篇文章](https://jmperezperez.com/medium-image-progressive-loading-placeholder/)详细分析了 Medium 图片渐进加载细节。
|
|
64
|
+
|
|
65
|
+
了解基本思路后,实现方式可以有很多种。其中关键,需要准备高清图对应的低质量图片,以及对低质量图片进行模糊处理。
|
|
66
|
+
|
|
67
|
+
### 低质量图片
|
|
68
|
+
|
|
69
|
+
BBKi.ng 所有图片均存放在阿里云 OSS 中。OSS 数据处理下的图片处理功能可以创建样式。不同样式对应不同的图片处理细节,如图片格式转换、图片质量、缩略等。我们可以新建名为 `LQIP` 的样式,将原图压缩为低质量图片。假设上传原图后得到的地址为:`url/to/my-awesome-picture.jpg`, 那么通过地址 `url/to/my-awesome-picture.jpg?x-oss-process=style/LQIP` 即能获取低质量图片。
|
|
70
|
+
|
|
71
|
+
### 模糊效果
|
|
72
|
+
|
|
73
|
+
实现图片模糊有很多种方式。比如
|
|
74
|
+
|
|
75
|
+
1. CSS [滤镜](https://developer.mozilla.org/en-US/docs/Web/CSS/filter)
|
|
76
|
+
2. CSS [背景滤镜](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop)
|
|
77
|
+
3. Canvas 渲染([blurhash](https://blurha.sh/))
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
img {
|
|
81
|
+
filter: blur(2px)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cover {
|
|
85
|
+
backdrop-filter: blur(2px);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
以上几种方式都各有优缺点。CSS 滤镜优点是写法比较简单,也无需引入新的元素。缺点是,边缘也会模糊掉。可以增加一个包裹元素,设置 `overflow: hidden` 解决,但是模糊半径较大时,图片边缘的模糊效果始终不是很理想([效果对比](https://codepen.io/z-j-h/pen/ZEJPEWq));用 CSS 背景滤镜可以得到锐利清晰的边缘,同时图片内容区域模糊效果也很好,缺点是效果是应用在元素背后的区域,因此我们需要引入额外的元素,同时该样式会影响图层合成时间、默认不兼容火狐浏览器;Blurhash 方法的优点是模糊图片只需要用简洁哈希字符串表示,节省空间。缺点是编码解码过程稍显繁琐。同时客户端解码大尺寸图片时也存在性能问题。
|
|
90
|
+
|
|
91
|
+
综合考虑后,最终使用背景滤镜对图片进行模糊处理。
|
|
92
|
+
|
|
93
|
+
**示例**
|
|
94
|
+
|
|
95
|
+
<ImgDemo />
|
|
96
|
+
|
|
97
|
+
## 参考链接
|
|
98
|
+
|
|
99
|
+
- https://web.dev/optimize-cls/
|
|
100
|
+
- https://medium.com/hd-pro/jpeg-formats-progressive-vs-baseline-73b3938c2339
|
|
101
|
+
- https://www.industrialempathy.com/posts/image-optimizations/
|
|
102
|
+
- https://www.guypo.com/introducing-lqip-low-quality-image-placeholders
|
|
103
|
+
- https://jmperezperez.com/medium-image-progressive-loading-placeholder/
|
|
104
|
+
- https://github.com/vercel/next.js/blob/canary/packages/next/client/image.tsx
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as 说明书 from "./bbking-manual.mdx";
|
|
2
|
+
import * as 加载动画 from "./loading.mdx";
|
|
3
|
+
import * as 回暖 from "./warming-up.mdx";
|
|
4
|
+
import * as 可以攻玉 from "./quote.mdx";
|
|
5
|
+
import * as 离开 from "./travel.mdx";
|
|
6
|
+
import * as 降温 from "./cooldown.mdx";
|
|
7
|
+
import * as 与或非禁区 from "./anti-logic.mdx";
|
|
8
|
+
import * as 没有女人的男人们 from "./men-without-women.mdx";
|
|
9
|
+
import * as 一天计划 from "./movie-day.mdx";
|
|
10
|
+
import * as 网站图片展示 from "./img.mdx";
|
|
11
|
+
import * as 大雪 from "./fall.mdx";
|
|
12
|
+
import * as 大寒 from "./major-cold.mdx";
|
|
13
|
+
import * as 春雨 from "./spring-rain.mdx";
|
|
14
|
+
import * as 春寒 from "./spring-cooldown.mdx";
|
|
15
|
+
import * as 入夏 from "./web-burnning.mdx";
|
|
16
|
+
import * as 六月 from "./black-river.mdx";
|
|
17
|
+
|
|
18
|
+
export const MdxArticleList = [
|
|
19
|
+
说明书,
|
|
20
|
+
网站图片展示,
|
|
21
|
+
// 加载动画,
|
|
22
|
+
与或非禁区,
|
|
23
|
+
可以攻玉,
|
|
24
|
+
离开,
|
|
25
|
+
降温,
|
|
26
|
+
没有女人的男人们,
|
|
27
|
+
一天计划,
|
|
28
|
+
大雪,
|
|
29
|
+
大寒,
|
|
30
|
+
回暖,
|
|
31
|
+
春雨,
|
|
32
|
+
春寒,
|
|
33
|
+
入夏,
|
|
34
|
+
六月,
|
|
35
|
+
];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 加载动画
|
|
3
|
+
tags:
|
|
4
|
+
- bbki.ng
|
|
5
|
+
- loading
|
|
6
|
+
- WebGL
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
import { SpinnerDemo } from "@/demo/SpinnerDemo";
|
|
10
|
+
import { DemoBox } from "@/demo/DemoBox";
|
|
11
|
+
|
|
12
|
+
<SpinnerDemo />
|
|
13
|
+
## 前言
|
|
14
|
+
>《她》是讲述在不远的未来人与人工智能相爱的科幻爱情电影。主人公西奥多(杰昆·菲尼克斯 Joaquin Phoenix 饰)是一位信件撰写人,心思细腻而深邃,能写出最感人肺腑的信件。他刚结束与妻子凯瑟琳(鲁妮·玛拉 Rooney Mara 饰)的婚姻,还没走出心碎的阴影。一次偶然机会让他接触到最新的人工智能系统 OS1 ……
|
|
15
|
+
|
|
16
|
+
相信大多数人,在看这部电影的时候,不仅仅会被剧情打动,也会被电影的镜头美学吸引。《她》呈现出一个没有被冷酷金属主导的科幻世界,那里光线柔和,色调温暖。人们的着装跟我们并没有太大区别,但是人工智能操作系统能完成的事情以及普及程度,现在看来很难想象。有意思的是,技术的发展并没有消灭「加载时间」,OS1 在安装的时候,西奥多依然需要等待。屏幕上精美的加载动画给我留下深刻的印象,从那时候起,就希望动画能出现在我的网站上。
|
|
17
|
+
|
|
18
|
+
## 灵感
|
|
19
|
+
根据 Atwood's Law
|
|
20
|
+
|
|
21
|
+
> Any application that _can_ be written in JavaScript, _will_ eventually be written in JavaScript.
|
|
22
|
+
|
|
23
|
+
肯定有人已经用 JS 实现 OS1 系统的加载动画。没错,这个人是 [@psyonline](https://codepen.io/psyonline),他的[在线示例](https://codepen.io/psyonline/pen/yayYWg)也非常惊艳。
|
|
24
|
+
那么,直接复制粘贴过来可以吗?该示例使用 Three.js 实现,写这篇文章的时候,根据 [bundlephobia](https://bundlephobia.com/package/three@0.142.0) 显示,`three@142.0` 压缩后仍然有 `596.9kB`。对于个人网站,仅仅用来实现一个加载动画,无疑很不划算。
|
|
25
|
+
|
|
26
|
+
[@shuding](https://twitter.com/shuding_)发布过一[系列推文](https://twitter.com/shuding_/status/1475916082875666441)介绍他如何制作一个轻量级的 [WebGL 地球库](https://github.com/shuding/cobe),十分精彩。开篇提及两个轻量级 WebGL 库:
|
|
27
|
+
|
|
28
|
+
- [phenomenon](https://github.com/vaneenige/phenomenon)
|
|
29
|
+
- [W](https://xem.github.io/W/)
|
|
30
|
+
|
|
31
|
+
前者压缩后,只有 `5.4KB`。正是我想找的工具。接下来,只需要 3 步就能实现文首的动画:
|
|
32
|
+
1. 熟悉 phenomenon 的 API
|
|
33
|
+
2. 用 phenomenon 绘制曲线
|
|
34
|
+
3. 让曲线动起来
|
|
35
|
+
|
|
36
|
+
## 行动
|
|
37
|
+
### WebGL
|
|
38
|
+
进行第一步时,发现自己还完全不了解 WebGL。看完 [The Book of Shaders](https://thebookofshaders.com/) 后,能基本理解 phenomenon 的[示例](https://codepen.io/collection/AOpMrm/)。至此,已经可以复制粘贴示例并修修改改,验证书中的概念和自己的理解。直到手痒痒想绘制自己想要的曲线,就进行到下一步。
|
|
39
|
+
|
|
40
|
+
### Curve
|
|
41
|
+
@psyonline [示例](https://codepen.io/psyonline/pen/yayYWg)中不难找到绘制曲线的代码:
|
|
42
|
+
```
|
|
43
|
+
function(percent) {
|
|
44
|
+
var x = length*Math.sin(pi2*percent)
|
|
45
|
+
, y = radius*Math.cos(pi2*3*percent)
|
|
46
|
+
, z
|
|
47
|
+
, t;
|
|
48
|
+
|
|
49
|
+
t = percent%0.25/0.25;
|
|
50
|
+
t = percent%0.25-(2*(1-t)*t* -0.0185 +t*t*0.25);
|
|
51
|
+
|
|
52
|
+
if (Math.floor(percent/0.25) == 0
|
|
53
|
+
|| Math.floor(percent/0.25) == 2) {
|
|
54
|
+
t *= -1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
z = radius*Math.sin(pi2*2* (percent-t));
|
|
58
|
+
|
|
59
|
+
return new THREE.Vector3(x, y, z);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
我的主要目标变成移植这段代码,其中 `pi2`, `length` 和 `radius` 均为常量,原文取值:
|
|
64
|
+
```
|
|
65
|
+
const length = 30;
|
|
66
|
+
const radius = 5.6;
|
|
67
|
+
const pi2 = Math.PI * 2;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
还有一个比较重要的参数 `percent`,打印发现取值范围为 0 到 1。有了这些,我愉快的写好 phenomenon 的 attributes:
|
|
71
|
+
```
|
|
72
|
+
const attributes = [
|
|
73
|
+
{
|
|
74
|
+
name: 'pos',
|
|
75
|
+
data: (index: number, total: number) => {
|
|
76
|
+
const percent = index / total;
|
|
77
|
+
// ...
|
|
78
|
+
// 复制粘贴 :)
|
|
79
|
+
// 此处省略
|
|
80
|
+
|
|
81
|
+
return [x, y, z];
|
|
82
|
+
},
|
|
83
|
+
size: 3,
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
然后得到一个空白的画布
|
|
88
|
+
<DemoBox />
|
|
89
|
+
|
|
90
|
+
因为对 WebGL 的了解依然接近于 0 在胡乱猜测并修改一通后,画布依然一片空白,于是沮丧放弃。直到晚上,突然想着将半径和长度都降低两个数量级,画布中终于出现一个扭曲的麻花。上次这么开心还是上一次。
|
|
91
|
+
|
|
92
|
+
### Matrix
|
|
93
|
+
现在可以回想一下大学课堂线性代数中矩阵运算的知识。回想不起来也没关系,如果有一个正在念大学的弟弟的话,他会帮助写出 `rotate` 方法。
|
|
94
|
+
```
|
|
95
|
+
export const VERTEX_SHADER = `
|
|
96
|
+
attribute vec3 pos;
|
|
97
|
+
uniform mat4 uProjectionMatrix;
|
|
98
|
+
uniform mat4 uModelMatrix;
|
|
99
|
+
uniform mat4 uViewMatrix;
|
|
100
|
+
uniform float uProgress;
|
|
101
|
+
mat4 rotate(float _angle){
|
|
102
|
+
return mat4(
|
|
103
|
+
1.0, 0.0, 0.0, 0.0,
|
|
104
|
+
0.0, cos(_angle), -sin(_angle), 0.0,
|
|
105
|
+
0.0, sin(_angle), cos(_angle), 0.0,
|
|
106
|
+
0.0, 0.0, 0.0, 1.0
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
void main(){
|
|
110
|
+
gl_Position = uProjectionMatrix
|
|
111
|
+
* uModelMatrix
|
|
112
|
+
* uViewMatrix
|
|
113
|
+
* rotate(uProgress)
|
|
114
|
+
* vec4(pos, 1.0);
|
|
115
|
+
gl_PointSize = 1.0;
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
这些碎片代码,和碎片想法,最终组装出心心念念的动画。
|
|
122
|
+
|
|
123
|
+
## 参考链接
|
|
124
|
+
1. https://codepen.io/psyonline/pen/yayYWg
|
|
125
|
+
2. https://twitter.com/shuding_/status/1475916082875666441
|
|
126
|
+
3. https://thebookofshaders.com/
|
|
127
|
+
4. https://shud.in/posts/metaballs-1
|
|
128
|
+
5. https://codepen.io/cvaneenige/pen/MGBZpB
|
|
129
|
+
6. https://movie.douban.com/subject/6722879/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 大寒
|
|
3
|
+
tags:
|
|
4
|
+
- 随笔
|
|
5
|
+
- 天气
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
冬天下半场,更冷了,而且多雨。新闻开始有暴雪预警,更南边的同学,春节返乡将经受疫和天气双重影响。
|
|
9
|
+
|
|
10
|
+
小区的地面,被雨水浇成黑色,香樟的树干也是。傍晚的时候,这种黑色像是从地面沿着树干伸向天空。黑色路面上的车辆,总是会压出一片黏腻的声音,比以往更吵。
|
|
11
|
+
|
|
12
|
+
上次雪停后,没再穿过厚羽绒服。身体在一点点适应湿寒。某天中午大雾,绕湖散步途中,被过于湿润的空气包裹着,有种安心的感觉。换城市工作后,一直在经历的不仅仅是环境温度持续下降,内心的一部分也在慢慢冷却。
|
|
13
|
+
|
|
14
|
+
和四季一同起伏,不当暴躁的怪物。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 没有女人的男人们
|
|
3
|
+
tags:
|
|
4
|
+
- 随笔
|
|
5
|
+
- 阅读
|
|
6
|
+
- 一天计划
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
《没有女人的男人们》是村上春树的短篇小说集,书本介绍写的是:「村上返回原点之作,《东京奇谭集》之后时隔九年又一短篇小说集」。「返回原点」也多少有点像在说自己,时隔三年又回到之前离开的城市。一天刚好看完。
|
|
10
|
+
|
|
11
|
+
| 上午 | 下午 | 晚上 |
|
|
12
|
+
|:----|:-----|:----|
|
|
13
|
+
| 驾驶我的车 <br /> 昨天 | 独立器官 <br /> 山鲁左德 <br /> 木野 | 恋爱的萨姆沙 |
|
|
14
|
+
|
|
15
|
+
故事中的男人们,有人妻子过世,有人想满世界流浪,有人是坚定的不婚主义,有人被戴上有颜色的帽子……男男女女,来来去去。短篇小说比起电影好像更任性一点,结局戛然而止也让人也觉得情理之中,根据交代过的信息想象一下男女主接下来的境遇也是一件很有意思的事情。我最喜欢的两篇是《山鲁佐德》和《木野》。前者作为一个故事,故事中又穿插了女主讲述的故事,很有层次感,也很容易被带入女主的玄妙变态、虚实莫辨的回忆或者说是想象中。甚至阅读完都不太想深究她和男主到底是什么关系。此外文中有一段关于胡子的描述,觉得挺好玩,感觉也可以尝试一下:
|
|
16
|
+
|
|
17
|
+
> 他之所以开始蓄胡子,主要是因为没有什么事情可做。如果有了胡子,他便可以经常把手放在下颔、鼻子下边或者鬓角,享受触摸的感觉。用剪刀和锑须刀修剪胡子的形状,也可以消磨时间。他这才发现,原来仅仅留个胡子,便能打发无聊。
|
|
18
|
+
|
|
19
|
+
《木野》直到结尾才对自己说,没错,我受伤了。一开始,故事男主好像都不是他一样,快结尾情节也开始有点莫名奇妙,就像拍摄过程中相机一直没有对焦。直到最后,木野对自己说,没错,我受伤了。读者可能才会和木野一起恍然而悟,是啊,他一开始就是个受伤的人啊。木野落跑逃避,沉默冷静,毫不在意。但最终他还是要正面盯着自己的创伤。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 个人电影节
|
|
3
|
+
tags:
|
|
4
|
+
- 随笔
|
|
5
|
+
- 电影
|
|
6
|
+
- 一天计划
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**一天计划**,用一天时间在同一个地点做同一件事情。比如写一天代码、看一天书。个人电影节也是「一天计划」的一部分。从之前标记想看的电影清单中随机挑选出五部电影,用一天时间看完。
|
|
10
|
+
|
|
11
|
+
| 上午 | 下午 | 晚上 |
|
|
12
|
+
|:----|:-----|:----|
|
|
13
|
+
| 花火 <br /> 数电影的人 | 不能说的游戏 <br /> 蜂蜜之地 | 尽情游戏 |
|
|
14
|
+
|
|
15
|
+
《花火》整部电影都很沉默冷静,甚至连剧中的枪响也是。可能是浓烈的情感需要大面积的留白去稀释,主角台词不多,却好像什么都已说尽。《数电影的人》是一部记录片,介绍了一群修复电影胶片的人,为电影延续生命。他们有枯燥的工作,有来自同事间互相鼓励和安慰。有意义危机,也有使命感和成就感。我更羡慕的是他们的工作周期,能花很长很长的一段时间慢慢把事情做好,也紧迫也从容。《不能说的游戏》讲述小女孩被熟人性侵后,整个人生受到的影响。电影配乐很欢快,大部分剪辑也是,但看似轻松欢快的节奏,却能让人感受到背后的压抑与主角内心困兽般的反应。这部电影,比起犯人,我更讨厌女主的母亲。《蜂蜜之地》是一部很美的记录片,贫瘠的土地,满眼只能看到岩石灌木和沙砾。相依为命的母女,贪婪的邻居。能够看到一种截然不同的生活方式,已经很心满意足。《尽情游戏》则是一部轻松的喜剧,可能包含了对战争的反思,我更愿意就把它当成一部普通的喜剧。最后其实还有一部德国电影《奇迹》,已经无法看下去,就像很久没有运动,跑完三公里已经到了极限,无法跑完最后一圈。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 可以攻玉
|
|
3
|
+
tags:
|
|
4
|
+
- quote
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
“生活曾经如此像游戏,在安谧甜美的气氛中,那些器具,都是玩具”
|
|
8
|
+
|
|
9
|
+
“(眼泪)不掉下来也没有关系,她其实是一个情绪。这就是电影,看的是微妙的神态,并不是结果。”
|
|
10
|
+
|
|
11
|
+
\- “我很悲惨,要是你知道我有多悲惨就好了。” \
|
|
12
|
+
\- “我们能做什么?我们必须过好自己的生活。”
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
“河流紀事 · 鄧獻誌
|
|
16
|
+
|
|
17
|
+
河流用一生梳理 \
|
|
18
|
+
地球的表情 \
|
|
19
|
+
沿岸收集人類的喜怒哀樂 \
|
|
20
|
+
檔案堆滿了海底的家”
|
|
21
|
+
|
|
22
|
+
"No, you can’t force other people to change. You can, however, change just about everything else. And usually, that’s enough."
|
|
23
|
+
[Aaron Swartz](http://www.aaronsw.com/weblog/nummi#:~:text=No%2C%20you%20can%E2%80%99t%20force%20other%20people%20to%20change.%20You%20can%2C%20however%2C%20change%20just%20about%20everything%20else.%20And%20usually%2C%20that%E2%80%99s%20enough.)
|
|
24
|
+
|
|
25
|
+
"The [hedonic treadmill](https://en.wikipedia.org/wiki/Hedonic_treadmill), also known as hedonic adaptation, is the observed tendency of humans to quickly return to a relatively stable level of happiness despite major positive or negative events or life changes."
|
|
26
|
+
|
|
27
|
+
"When people look at my pictures I want them to feel the way they do when they want to read a line of a poem twice."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 离开
|
|
3
|
+
tags:
|
|
4
|
+
- 随笔
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
“人和事儿都在命运的秩序里展开,让我猝不及防,目瞪口呆。也就是这个时候,才发现自己已经长大成人,我觉得我必须离开。”
|
|
8
|
+
|
|
9
|
+
虽然还没过完,二零二一年已经可以总结成:重大决定最密集的一年。这些决定既不慎重也不轻率,就像迷宫中的老鼠,有个角落放着它喜欢的奶酪,循味靠近的过程中不觉就走过很多需要左右抉择的路口。
|
|
10
|
+
|
|
11
|
+
妈妈发消息说车库里的行李可能被水泡了,家里下大雨。寄回家的行李让弟弟就近堆在车库里,那天他要去考科目二。晚上,表弟特意过去把完好纸箱子都摞到凳子上。几天后,我回到家把行李都整理出来,一个路过的老婆婆问我纸箱子还要不要,我给了她。
|
|
12
|
+
|
|
13
|
+
回家第一次见到侄子,他刚过两岁生日没多久。可爱是可爱,顽皮也顽皮。会口齿不清说一些很短词语,只有在举着酸奶喊干杯的时候,如银瓶乍破同时字正腔圆。她奶奶说他长大绝对是个酒鬼,你们看吧。表弟则惯着他,吃饭时把他抱在腿上,任由他用食指和拇指去酒杯里捏着酒往嘴里送。隔天把他从客厅的小天地带去水上乐园玩,一路吹着风特别乖巧,不声不响。很久之后这段经历会在他两岁的的记忆里发酵成什么样子呢?他是不是会在某天,跟朋友喊着干杯想起两岁的时候,他赖在浅水区不肯上岸回家的情景。他是否还记得飘在泳池上方的彩带,是否还记得他爸抱着他冲下水滑梯重力失常的感觉。
|
|
14
|
+
|
|
15
|
+
小电驴跟县城很配。和弟弟每天骑着它,在外婆家和自己家穿梭。鼎山脚下的路上上下下,我自己脚下的路也是。早上穿着拖鞋去马路对面吃浇头喧宾夺主的三鲜粉,中午和带娃的表弟喝酒,晚上回家和弟弟一起看电影。如果不是前途未卜,现在的生活何尝不是向往的生活。空闲时间刷着省会城市的工作机会和房源,在踏实的当下和虚无的未来不断切换。生活有时候好像一成不变,有时候如梦似幻。一成不变的时候,如陷囹圄。瞬息破灭的时候,如坠深渊。聚散之后,定睛细看,有些碎片一直没有离开过,比如大学里买的帆布包和键盘。
|
|
16
|
+
|
|
17
|
+
一些云端存储应用保留着过去的照片。一直觉得照片是回忆的快捷键,它们又像一个个虫洞通往过去的时空。这些虫洞中,有几个反复在脑海涌现。湘江护栏边坐着靠椅钓鱼喝茶的大叔,我记得那天傍晚有风,江面粼粼。小洲前的红色帆船,水天被一线小洲和绿树隔断。那是一个秋天,烟霏云敛。凭着这些清晰又模糊的印象,我在找房应用上不断重组筛选条件。动身前一晚,终于确定下来收藏房源和看房次序。
|
|
18
|
+
|
|
19
|
+
动身回老家前,还有过一段过渡时间。房子到期感情也到期,工作即使辞职也需要临时的落脚地。终于明白为什么即使身处战争,士兵也需要营地。我也需要一个休息和思考的营地,同时因为仓促搬家,无比厌恶对一个地方产生依赖。室友听说我的处境后,在生日那天打车赶过来,在他之前住过的地方为我找到住处。行李被货车拉到楼下,很难不想起三年前只背着一个包就来到这座城市的场景。在营地休整了好一阵,最终还是决定离开这座城市离开这里,这座迷宫没有老鼠喜欢的奶酪。营地附近,有个正在备孕的同学,约好隔天慢跑。百忧撼心的营地生活,养成慢跑习惯成了为数不多的意外收获。
|
|
20
|
+
|
|
21
|
+
找房的过程比想象顺利,事实上只看完第一家就已经决定签订合同。在小区穿行的感觉很对,我需要这种老旧的氛围平复内心的戾气。后来朋友告诉我说,这是河西最老的小区。等房东和中介离开,我开始对附近进行简单的探索,走出小区门口竟然就到了江边。往东偏北望去,我看到了回忆快捷键中的小洲!住进了自己的回忆里。蓦然回首的感觉大抵如此了。在地图应用中得知小洲叫傅家洲。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Navigate } from "react-router-dom";
|
|
3
|
+
import { useSupabaseSession } from "@/hooks/use_supa_session";
|
|
4
|
+
|
|
5
|
+
export const AuthRequired = (props: {
|
|
6
|
+
children: any;
|
|
7
|
+
shouldRedirect?: boolean;
|
|
8
|
+
shouldBeKing?: boolean;
|
|
9
|
+
}) => {
|
|
10
|
+
const { access_token: token, isKing } = useSupabaseSession() || {};
|
|
11
|
+
|
|
12
|
+
if (!token || (props.shouldBeKing && !isKing)) {
|
|
13
|
+
return props.shouldRedirect ? <Navigate to={"/login"} /> : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return props.children;
|
|
17
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useContext, useEffect } from "react";
|
|
2
|
+
import { GlobalLoadingContext } from "@/global_loading_state_provider";
|
|
3
|
+
import { LoadingSpiral } from "@bbki.ng/components";
|
|
4
|
+
|
|
5
|
+
export const Spinner = (props: any) => {
|
|
6
|
+
const {
|
|
7
|
+
disableDotIndicator,
|
|
8
|
+
multiplier = 10000,
|
|
9
|
+
color = [209, 213, 219, 1],
|
|
10
|
+
offset = -0.3,
|
|
11
|
+
length = 0.3,
|
|
12
|
+
} = props;
|
|
13
|
+
const { setIsLoading } = useContext(GlobalLoadingContext);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (disableDotIndicator) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
return () => {
|
|
21
|
+
setIsLoading(false);
|
|
22
|
+
};
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<LoadingSpiral
|
|
27
|
+
multiplier={multiplier}
|
|
28
|
+
color={color}
|
|
29
|
+
offset={offset}
|
|
30
|
+
length={length}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { ReactElement } from "react";
|
|
2
|
+
import { Tags, Article } from "@bbki.ng/components";
|
|
3
|
+
import { ROUTES } from "@/constants";
|
|
4
|
+
|
|
5
|
+
type ArticlePageProps = {
|
|
6
|
+
tags?: string[];
|
|
7
|
+
title: string;
|
|
8
|
+
description?: any;
|
|
9
|
+
headless?: boolean;
|
|
10
|
+
children: ReactElement;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const ArticlePage = (props: ArticlePageProps) => {
|
|
14
|
+
const { tags: tagNames, title, description, headless } = props;
|
|
15
|
+
const tags = tagNames
|
|
16
|
+
? tagNames.map((t) => ({ children: t, to: `${ROUTES.TAGS}/${t}` }))
|
|
17
|
+
: [];
|
|
18
|
+
|
|
19
|
+
if (headless) {
|
|
20
|
+
return props.children;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<Article title={title} description={description}>
|
|
26
|
+
<article className="prose mb-20">{props.children}</article>
|
|
27
|
+
</Article>
|
|
28
|
+
{tagNames && <Tags tags={tags} />}
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
type aspectRatioBoxProps = {
|
|
5
|
+
width: number | string;
|
|
6
|
+
hwRatio: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
children?: any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const AspectRatioBox = (props: aspectRatioBoxProps) => {
|
|
12
|
+
const { className, width, hwRatio, children } = props;
|
|
13
|
+
const innerStyle = {
|
|
14
|
+
width: "100%",
|
|
15
|
+
paddingTop: `${hwRatio * 100}%`,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
style={{ width, maxWidth: "100%" }}
|
|
21
|
+
className={classnames(className, "relative")}
|
|
22
|
+
>
|
|
23
|
+
<div style={innerStyle} />
|
|
24
|
+
{children && (
|
|
25
|
+
<div className="absolute top-0 bottom-0 left-0 right-0">{children}</div>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { TextColors } from "@/types/color";
|
|
4
|
+
|
|
5
|
+
type blurCoverProps = {
|
|
6
|
+
textColor?: TextColors;
|
|
7
|
+
className?: string;
|
|
8
|
+
children: any;
|
|
9
|
+
height?: number;
|
|
10
|
+
visibleOnHover?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const BlurCover = (props: blurCoverProps) => {
|
|
14
|
+
const { textColor, children, className, height = 50, visibleOnHover } = props;
|
|
15
|
+
const cls = classnames(
|
|
16
|
+
className,
|
|
17
|
+
textColor,
|
|
18
|
+
"blur-cover",
|
|
19
|
+
"absolute",
|
|
20
|
+
"w-full",
|
|
21
|
+
"flex items-center justify-center"
|
|
22
|
+
);
|
|
23
|
+
return (
|
|
24
|
+
<div className={cls} style={{ height }}>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Table, Link, Error } from "@bbki.ng/components";
|
|
3
|
+
import { useBooks } from "@/hooks";
|
|
4
|
+
import { TableSkeleton } from "@/components/table_skeleton";
|
|
5
|
+
|
|
6
|
+
const CELL_STYLE = {
|
|
7
|
+
width: 100,
|
|
8
|
+
maxWidth: 100,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const BookList = () => {
|
|
12
|
+
const { books, isLoading, isError } = useBooks();
|
|
13
|
+
if (isError) {
|
|
14
|
+
return <Error error={isError} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (isLoading) {
|
|
18
|
+
return <TableSkeleton headers={["书名", "状态"]} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const renderHeader = () => {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<Table.HCell style={CELL_STYLE}>书名</Table.HCell>
|
|
25
|
+
<Table.HCell style={CELL_STYLE}>状态</Table.HCell>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const renderRow = (index: number) => {
|
|
31
|
+
const { name: title, link, status } = books[index];
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<Table.Cell style={CELL_STYLE}>
|
|
35
|
+
<Link to={link} external>
|
|
36
|
+
{title}
|
|
37
|
+
</Link>
|
|
38
|
+
</Table.Cell>
|
|
39
|
+
<Table.Cell style={CELL_STYLE}>{status}</Table.Cell>
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
return (
|
|
44
|
+
<Table
|
|
45
|
+
rowCount={books.length}
|
|
46
|
+
rowRenderer={renderRow}
|
|
47
|
+
headerRenderer={renderHeader}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|