优化文件结构
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: 和朋友们的故事
|
||||
cover: https://pic.biss.click/image/eb65954d-3076-41bf-b890-c357884231c7.webp
|
||||
category: 杂谈
|
||||
tags: 生活
|
||||
abbrlink: a31d95d9
|
||||
date: 2026-01-05 13:14:16
|
||||
summary: >-
|
||||
这篇文章讲述了作者与朋友们的故事,以及他通过挂号信和电子贺卡与朋友们保持联系的经历。作者描述了自己对这种传统通信方式的喜爱,尽管现在网络发达,但他仍然觉得挂号信具有独特的魅力。文章中提到了作者去年上半年开始给朋友们写信,下半年因忙碌而减少,年底时尝试使用AI工具创作电子贺卡。作者感慨大学时间的快速流逝,以及朋友们之间的疏远。尽管如此,作者认为这是必然的事情,并无悲伤之处,只是单纯的感慨。最后,作者表示下次有时间会在文章中更新更多内容。
|
||||
---
|
||||
|
||||
最近一直忙于学习(~~虽然也没怎么好好学~~),所以一直忘了网站的更新;现在闲下来了,又不知道该写点什么;就写写我和朋友们的故事吧。
|
||||
因为一次偶然的突发奇想,解锁了挂号信这种奇奇怪怪的通信方式,虽然现在网络发达,但感觉这种方式有一种独特的魅力。所以从去年开始就断断续续地给朋友们写信,这是一些挂号信收据:
|
||||
<center><img src="https://pic.biss.click/i/2026/01/05/y08m0d.jpg" alt="一些挂号信收据" /></center>
|
||||
这应该是去年上半年的一张图片,下半年也在零散的发,只不过下半年比较忙,没发多少。到年底的时候呢又想着写一点贺年信,当然感觉自己的水平下降,写不出什么好的文章。于是开始着手准备电子版贺卡,当然完全使用了AI工具,自己写太麻烦,也没什么精力去写,于是就有了下面的电子贺卡:
|
||||
<center><img src="https://pic.biss.click/i/2026/01/05/ys91t1.png" alt="电子贺卡" /></center>
|
||||
之后呢,又写了其他一些有趣的功能,但好像不是这篇文章的重点;
|
||||
再写信和创作电子贺卡是感慨万分,进入大学之后时间似乎变得十分快,感觉除了几个天天联系的好友之外,其他人都慢慢疏远了,不过这也是必然的事情,也没什么可悲伤的,只是感慨罢了。
|
||||
怎么说,就这样吧,下次有时间再在这篇文章里更新。
|
||||
@@ -0,0 +1,291 @@
|
||||
---
|
||||
title: 添加网站左上角菜单
|
||||
cover: https://pic.biss.click/image/a40baba1-b296-4b3b-a781-da45bffb1b95.webp
|
||||
categories: 建站手札
|
||||
series: webcustom
|
||||
tags:
|
||||
- 网站
|
||||
abbrlink: '7e921903'
|
||||
summary: >-
|
||||
这篇文章详细介绍了如何在柳神的网站上添加一个自定义的左上角菜单,包括菜单的结构、样式以及响应式设计。作者提供了具体的HTML和CSS代码示例,展示了如何实现一个包含多个链接和功能的导航栏,同时确保在不同屏幕尺寸下的适配性。此外,还解释了一些关键的CSS属性和JavaScript交互效果,以实现平滑的菜单显示和隐藏。通过这些步骤,用户可以轻松地自定义和美化网站的顶部导航栏,提升用户体验。
|
||||
date: 2026-02-10 07:11:14
|
||||
---
|
||||
|
||||
看到柳神的网站有这种菜单,但是没有写魔改教程,只好自己慢慢摸索了。
|
||||
|
||||
{% link liushen,liushen,https://blog.liushen.fun %}
|
||||
|
||||
预计做好后是这种效果:
|
||||
|
||||

|
||||
|
||||
首先,修改```\themes\butterfly\layout\includes\header\nav.pug```:
|
||||
```pug
|
||||
nav#nav
|
||||
//- 左侧区域:包含指纹菜单和网站名
|
||||
span#blog-info
|
||||
#ls-menu-container
|
||||
i.fas.fa-fingerprint
|
||||
#ls-menu-panel
|
||||
.ls-section
|
||||
.ls-title 😀 个人网站
|
||||
.ls-grid
|
||||
a(href="/") #[i.fas.fa-rss] 个人博客
|
||||
a(href="https://github.com/bishshi") #[i.fab.fa-github] Github
|
||||
.ls-section
|
||||
.ls-title 😎 常用服务
|
||||
.ls-grid
|
||||
a(href="https://git.biss.click/biss") #[i.fas.fa-code] 代码仓库
|
||||
a(href="https://mm.biss.click") #[i.fas.fa-pen-nib] 日常yy
|
||||
a(href="https://statstic.biss.click") #[i.fas.fa-users] 访客统计
|
||||
a(href="https://pic.biss.click") #[i.fas.fa-image] 图床
|
||||
a(href="https://chat.biss.click") #[i.fas.fa-robot] AI网站
|
||||
.ls-section
|
||||
.ls-title 🛸 实用工具
|
||||
.ls-grid
|
||||
a(href="https://cover.biss.click") #[i.fas.fa-palette] 封面设计
|
||||
|
||||
a.nav-site-title(href=url_for('/'))
|
||||
if theme.nav.logo
|
||||
img.site-icon(src=url_for(theme.nav.logo) alt='Logo')
|
||||
if theme.nav.display_title
|
||||
span.site-name=config.title
|
||||
|
||||
//- 中间区域:关键!必须在 nav 下一级,以便 JS 切换类名
|
||||
if globalPageType === 'post' && theme.nav.display_post_title
|
||||
a.nav-page-title(href='javascript:void(0);' onclick='btf.scrollToDest(0, 500)')
|
||||
span.site-name=(page.title || config.title)
|
||||
|
||||
//- 右侧区域
|
||||
#nav-right
|
||||
if theme.menu
|
||||
#menus
|
||||
!= partial('includes/header/menu_item', {}, {cache: true})
|
||||
|
||||
if theme.search.use || true
|
||||
#random-post-button
|
||||
a.site-page.social-icon#random-post-link(href='javascript:void(0);' onclick='randomPost()')
|
||||
i.fas.fa-solid.fa-shuffle
|
||||
#search-button
|
||||
a.site-page.social-icon.search-typesense-trigger
|
||||
i.fas.fa-search.fa-fw
|
||||
#toggle-menu
|
||||
span.site-page
|
||||
i.fas.fa-bars.fa-fw
|
||||
```
|
||||
|
||||
在合适的目录下新建```nav.css```(例如```\themes\butterfly\source\css\nav.css```),这份css是磨砂玻璃的样式:
|
||||
|
||||
```css
|
||||
#nav-right{
|
||||
flex:1 1 auto;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-wrap:nowrap;
|
||||
}
|
||||
|
||||
/* 导航栏居中 */
|
||||
|
||||
#sidebar #sidebar-menus .menus_items .menus_item {
|
||||
margin: 10px 0;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_items a.site-page {
|
||||
padding-left: 0;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_items .site-page {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 6px 30px 6px 22px;
|
||||
color: var(--font-color);
|
||||
font-size: 1.15em;
|
||||
border: var(--style-border-always);
|
||||
background: var(--icat-card-bg);
|
||||
font-size: 14px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
#sidebar #sidebar-menus .menus_items .site-page i:first-child {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#nav #menus {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
#nav #blog-info {
|
||||
flex-wrap: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 102;
|
||||
max-width: fit-content;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
#nav {
|
||||
padding: 0 15px;
|
||||
}
|
||||
#nav-group {
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
#rightside {
|
||||
right: -42px;
|
||||
}
|
||||
}
|
||||
/* IPAD菜单栏调整 */
|
||||
|
||||
/* 1. 容器溢出穿透 */
|
||||
#nav, #blog-info, #nav-group {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
#ls-menu-container {
|
||||
position: relative !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
cursor: pointer !important;
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
|
||||
/* 2. 悬浮面板:宽大且高不透明度 */
|
||||
#ls-menu-panel {
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
margin-top: 15px !important;
|
||||
width: 420px;
|
||||
padding: 24px;
|
||||
border-radius: 18px;
|
||||
|
||||
/* 高不透明度磨砂玻璃 (0.9) */
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
backdrop-filter: blur(20px) saturate(180%) !important;
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.5) !important;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18) !important;
|
||||
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
transform: translateY(12px) !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
z-index: 999999 !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 3. 透明感应桥梁 */
|
||||
#ls-menu-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 25px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 4. 触发效果 */
|
||||
#ls-menu-container:hover #ls-menu-panel {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
#ls-menu-container:hover i.fa-fingerprint {
|
||||
color: #49b1f5;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 5. 内部网格:保持两列 */
|
||||
.ls-section { margin-bottom: 22px; }
|
||||
.ls-section:last-child { margin-bottom: 0; }
|
||||
.ls-title {
|
||||
color: #333;
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.ls-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr); /* 恢复为两列 */
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ls-grid a {
|
||||
color: #444 !important;
|
||||
font-size: 14px !important;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.ls-grid a:hover {
|
||||
background: #49b1f5 !important;
|
||||
color: #fff !important;
|
||||
transform: translateX(4px); /* 悬停时轻微右移,增加动感 */
|
||||
}
|
||||
|
||||
.ls-grid a i {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 6. 深色模式适配 */
|
||||
[data-theme='dark'] #ls-menu-panel {
|
||||
background: rgba(30, 30, 30, 0.95) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
[data-theme='dark'] .ls-title { color: #eee; }
|
||||
[data-theme='dark'] .ls-grid a {
|
||||
color: #ccc !important;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
#nav.show-title .nav-page-title {
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
position: absolute !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
white-space: nowrap !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
#nav.show-title #menus,
|
||||
#nav.show-title .nav-site-title {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#nav .nav-page-title {
|
||||
display: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#nav #blog-info {
|
||||
overflow: visible !important;
|
||||
}
|
||||
```
|
||||
|
||||
然后在```_config.butterfly.yml```里面引用该css
|
||||
|
||||
```yml
|
||||
- <link rel="stylesheet" href="/css/nav.css">
|
||||
```
|
||||
|
||||
然后重新构建应该就可以看到效果了。
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: 自建renovate-bot
|
||||
categories:
|
||||
- 技术
|
||||
cover: https://pic.biss.click/image/fca16741-64fa-495b-aa5e-a2ef077461ef.webp
|
||||
swiper_index: 3
|
||||
tags:
|
||||
- gitea
|
||||
abbrlink: 56f57c0b
|
||||
summary: >-
|
||||
这篇文章介绍了如何自建Renovate Bot以自动更新依赖,并通过Docker Compose进行部署。首先,文章提供了Renovate
|
||||
Bot的Docker Compose文件配置,包括使用特定平台(gitea)、设置Git
|
||||
API地址、生成Token以及启用自动发现功能。接着,建议创建一个专门用于管理Renovate
|
||||
Bot的账户,并为其生成登录Token。最后,通过在系统的crontab中设置定时任务,可以自动化运行更新流程。
|
||||
date: 2026-02-23 10:49:17
|
||||
---
|
||||
|
||||
最近自建了`gitea`,所以把`renovatebot`也自建一下,毕竟`renovatebot`可以自动更新依赖,但是它不对自建`git`提供服务。
|
||||
这是`docker compose`文件:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
renovate:
|
||||
image: renovate/renovate:latest
|
||||
environment:
|
||||
RENOVATE_PLATFORM: 'gitea'
|
||||
RENOVATE_ENDPOINT: '你的git api地址'
|
||||
RENOVATE_TOKEN: 'token'
|
||||
RENOVATE_AUTODISCOVER: 'true'
|
||||
RENOVATE_GIT_AUTHOR: Renovate Bot <bot@biss.click>
|
||||
RENOVATE_USERNAME: renovate-bot
|
||||
volumes:
|
||||
- ./renovate-data:/tmp/renovate
|
||||
```
|
||||
|
||||
可以新建一个`renovate`用户来专门管理,然后登录这个账户创建一个登录`token`。然后再需要使用这个的仓库添加这个用户为协作者就可以了。
|
||||
这个`docker`容器在运行后会自动退出,这是正常的。
|
||||
可以在系统crontab新建一个定时任务,这样就可以自动运行更新。
|
||||
```bash
|
||||
cd /www/compose/renovatebot && docker compose run --rm renovate
|
||||
```
|
||||
|
||||
{% link 官方文档,Renovatebot,https://docs.renovatebot.com/examples/self-hosting/ %}
|
||||
@@ -0,0 +1,721 @@
|
||||
---
|
||||
title: 添加typesense搜索
|
||||
categories:
|
||||
- 技术
|
||||
- 建站手札
|
||||
abbrlink: f287c563
|
||||
summary: >-
|
||||
这篇文章介绍了如何在博客中集成Typesense搜索引擎,包括安装Typesense、添加数据集以及同步数据的详细步骤。首先,通过命令行安装Typesense并配置相关参数,如API密钥和数据目录。接着,安装必要的npm包,并在Hexo博客中配置搜索功能,指定存储搜索数据的JSON文件路径。然后,编写了一个同步脚本,用于读取XML文件、解析数据、检查或创建集合以及导入数据到Typesense。最后,在博客底部添加了InstantSearch和Typesense的JavaScript库,并根据实际情况修改了配置。
|
||||
date: 2026-02-05 13:14:16
|
||||
series: webcustom
|
||||
tags:
|
||||
---
|
||||
|
||||
最近在构建班级博客,用```ghost cms```,在构建搜索时发现了typesense,所以把他移植到这个博客上。
|
||||
|
||||
# 安装typesense
|
||||
|
||||
直接用```docker-compose```:
|
||||
|
||||
```yaml
|
||||
|
||||
services:
|
||||
typesense:
|
||||
image: typesense/typesense:30.1
|
||||
restart: always
|
||||
ports:
|
||||
- "8108:8108"
|
||||
volumes:
|
||||
- ./typesense-data:/data
|
||||
command: '--data-dir /data --api-key=填写key --enable-cors'
|
||||
|
||||
```
|
||||
|
||||
然后就是反向代理之类的,不过多写了。
|
||||
|
||||
# 添加数据集
|
||||
|
||||
```bash
|
||||
# 先安装库
|
||||
npm install hexo-generator-search
|
||||
npm install typesense xml2js
|
||||
```
|
||||
然后在```config.yml```配置(就是把文章生成json):
|
||||
|
||||
```yml
|
||||
search:
|
||||
path: search.json
|
||||
field: post
|
||||
content: true
|
||||
```
|
||||
|
||||
创建一个数据同步脚本```sync_typesense.js```:
|
||||
|
||||
```js
|
||||
const Typesense = require('typesense');
|
||||
const fs = require('fs');
|
||||
const xml2js = require('xml2js');
|
||||
|
||||
// --- 配置区域 ---
|
||||
const CONFIG = {
|
||||
apiKey: '你的Admin-API-Key', // 必须是 Admin Key
|
||||
host: '你的Typesense主机地址',
|
||||
port: 443,
|
||||
protocol: 'https',
|
||||
collectionName: 'blogs'
|
||||
};
|
||||
|
||||
const client = new Typesense.Client({
|
||||
'nodes': [{ 'host': CONFIG.host, 'port': CONFIG.port, 'protocol': CONFIG.protocol }],
|
||||
'apiKey': CONFIG.apiKey,
|
||||
'connectionTimeoutSeconds': 5
|
||||
});
|
||||
|
||||
async function sync() {
|
||||
try {
|
||||
// 1. 读取并解析 XML
|
||||
const xml = fs.readFileSync('./public/search.xml', 'utf8');
|
||||
const parser = new xml2js.Parser({ explicitArray: false });
|
||||
const result = await parser.parseStringPromise(xml);
|
||||
|
||||
// 提取文章列表 (处理单篇文章和多篇文章的情况)
|
||||
let entries = result.search.entry;
|
||||
if (!Array.isArray(entries)) entries = [entries];
|
||||
|
||||
// 格式化数据以适配 Typesense
|
||||
const documents = entries.map(post => ({
|
||||
title: post.title,
|
||||
url: post.url,
|
||||
content: post.content,
|
||||
categories: post.categories ? (Array.isArray(post.categories.category) ? post.categories.category : [post.categories.category]) : [],
|
||||
tags: post.tags ? (Array.isArray(post.tags.tag) ? post.tags.tag : [post.tags.tag]) : [],
|
||||
}));
|
||||
|
||||
// 2. 检查或创建 Collection (Schema)
|
||||
try {
|
||||
await client.collections(CONFIG.collectionName).retrieve();
|
||||
} catch (err) {
|
||||
const schema = {
|
||||
name: CONFIG.collectionName,
|
||||
fields: [
|
||||
{ name: 'title', type: 'string', locale: 'zh'},
|
||||
{ name: 'content', type: 'string', locale: 'zh'},
|
||||
{ name: 'url', type: 'string' },
|
||||
{ name: 'categories', type: 'string[]', facet: true },
|
||||
{ name: 'tags', type: 'string[]', facet: true }
|
||||
]
|
||||
};
|
||||
await client.collections().create(schema);
|
||||
console.log('Collection created!');
|
||||
}
|
||||
|
||||
// 3. 导入数据 (使用 upsert 模式:存在则更新,不存在则创建)
|
||||
console.log(`Syncing ${documents.length} posts to Typesense...`);
|
||||
await client.collections(CONFIG.collectionName).documents().import(documents, { action: 'upsert' });
|
||||
console.log('Sync complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
sync();
|
||||
```
|
||||
然后运行```node sync_typesense.js```
|
||||
|
||||
# 创建只读key
|
||||
把下面代码存储成js,node运行就行。
|
||||
```js
|
||||
const http = require('https');
|
||||
|
||||
const data = JSON.stringify({
|
||||
"description": "Public search only key",
|
||||
"actions": ["documents:search"],
|
||||
"collections": ["blogs"]
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: '', // 不要带 https://
|
||||
port: 443,
|
||||
path: '/keys',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-TYPESENSE-API-KEY': '你的admin key',
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': data.length
|
||||
}
|
||||
};
|
||||
|
||||
const req = http.request(options, res => {
|
||||
res.on('data', d => { process.stdout.write(d); });
|
||||
});
|
||||
|
||||
req.on('error', error => { console.error(error); });
|
||||
req.write(data);
|
||||
req.end();
|
||||
```
|
||||
|
||||
# 博客添加搜索
|
||||
|
||||
在```config.yaml```inject bottom添加:
|
||||
|
||||
```yaml
|
||||
- <script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script>
|
||||
- <script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>
|
||||
```
|
||||
|
||||
为了方便,我直接修改了```\themes\butterfly\source\js\search\local_search.js```
|
||||
|
||||
```js
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// 配置区域 - 请根据实际情况修改
|
||||
// ============================================================================
|
||||
const CONFIG = {
|
||||
apiKey: "", // ⚠️ 建议使用 Search-Only API Key
|
||||
server: {
|
||||
host: "host",
|
||||
port: "443",
|
||||
protocol: "https"
|
||||
},
|
||||
indexName: "blogs",
|
||||
searchParams: {
|
||||
query_by: "title,content",
|
||||
highlight_full_fields: "title,content",
|
||||
per_page: 8,
|
||||
num_typos: 1,
|
||||
typo_tokens_threshold: 1,
|
||||
prefix: true
|
||||
},
|
||||
ui: {
|
||||
maxRetries: 10,
|
||||
retryDelay: 100,
|
||||
animationDuration: 300
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 状态管理
|
||||
// ============================================================================
|
||||
let searchInstance = null;
|
||||
let isInitialized = false;
|
||||
let isSearchOpen = false;
|
||||
let initRetryCount = 0;
|
||||
const MAX_INIT_RETRIES = 30; // 最多重试30次 (3秒)
|
||||
|
||||
// ============================================================================
|
||||
// 错误提示函数
|
||||
// ============================================================================
|
||||
function showErrorMessage() {
|
||||
const hitsContainer = document.getElementById('hits');
|
||||
if (!hitsContainer) return;
|
||||
|
||||
hitsContainer.innerHTML =
|
||||
'<div class="ts-empty">' +
|
||||
'<div style="color: #f44336;"><i class="fas fa-exclamation-triangle" style="font-size: 3rem;"></i></div>' +
|
||||
'<div style="font-size: 1.1rem; font-weight: bold; margin: 15px 0;">搜索服务加载失败</div>' +
|
||||
'<div style="font-size: 0.9rem; color: #666; line-height: 1.8;">' +
|
||||
'<p>依赖库未能正确加载,请检查以下配置:</p>' +
|
||||
'<ol style="text-align: left; max-width: 500px; margin: 15px auto;">' +
|
||||
'<li>确认已在 <code>_config.butterfly.yml</code> 中正确引入依赖</li>' +
|
||||
'<li>检查 JS 文件加载顺序(先 instantsearch.js,再 adapter)</li>' +
|
||||
'<li>尝试更换 CDN 或使用本地文件</li>' +
|
||||
'<li>打开浏览器控制台查看详细错误信息</li>' +
|
||||
'</ol>' +
|
||||
'</div>' +
|
||||
'<div style="margin-top: 20px;">' +
|
||||
'<button onclick="location.reload()" style="padding: 10px 20px; background: #49b1f5; color: white; border: none; border-radius: 5px; cursor: pointer;">重新加载页面</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 1. 动态插入 HTML 结构
|
||||
// ============================================================================
|
||||
const searchHTML = `
|
||||
<div id="typesense-search-mask" class="ts-mask" style="display:none;">
|
||||
<div id="typesense-search-container" class="ts-container">
|
||||
<div class="ts-header">
|
||||
<span class="ts-title">
|
||||
<i class="fas fa-search"></i> 本站搜索
|
||||
</span>
|
||||
<span id="close-typesense" class="ts-close" aria-label="关闭搜索">×</span>
|
||||
</div>
|
||||
<div id="searchbox"></div>
|
||||
<div id="stats" class="ts-stats"></div>
|
||||
<div id="hits" class="ts-hits"></div>
|
||||
<div id="pagination" class="ts-pagination"></div>
|
||||
<div class="ts-footer">
|
||||
<small>Search powered by <strong>Typesense</strong></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ts-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.ts-mask.active { opacity: 1; }
|
||||
.ts-container {
|
||||
margin: 5% auto;
|
||||
width: 90%;
|
||||
max-width: 650px;
|
||||
background: var(--search-bg, var(--card-bg, #fff));
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 10001;
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
.ts-mask.active .ts-container {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
.ts-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid var(--text-highlight-color, #49b1f5);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.ts-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: var(--text-highlight-color, #49b1f5);
|
||||
}
|
||||
.ts-close {
|
||||
cursor: pointer;
|
||||
font-size: 28px;
|
||||
color: var(--font-color, #333);
|
||||
line-height: 1;
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
}
|
||||
.ts-close:hover {
|
||||
color: var(--text-highlight-color, #49b1f5);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.ais-SearchBox-input {
|
||||
position: relative;
|
||||
z-index: 10002;
|
||||
cursor: text;
|
||||
padding: 12px 40px 12px 15px !important;
|
||||
border-radius: 8px !important;
|
||||
border: 2px solid #eee !important;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
background: var(--card-bg, #fff);
|
||||
color: var(--font-color, #333);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.ais-SearchBox-input:focus {
|
||||
border-color: var(--text-highlight-color, #49b1f5) !important;
|
||||
box-shadow: 0 0 0 3px rgba(73, 177, 245, 0.1);
|
||||
}
|
||||
.ts-stats {
|
||||
margin: 10px 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--font-color, #666);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.ts-hits {
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
margin-top: 15px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.ts-hits::-webkit-scrollbar { width: 6px; }
|
||||
.ts-hits::-webkit-scrollbar-track {
|
||||
background: var(--card-bg, #f1f1f1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.ts-hits::-webkit-scrollbar-thumb {
|
||||
background: var(--text-highlight-color, #49b1f5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.ts-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--font-color, #999);
|
||||
}
|
||||
.ts-empty i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.ts-empty code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
color: #e91e63;
|
||||
}
|
||||
.ts-empty ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.ts-empty li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.ts-result-item {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
border: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
background: var(--card-bg, #fff);
|
||||
}
|
||||
.ts-result-item:hover {
|
||||
background: var(--text-bg-hover, rgba(73, 177, 245, 0.05));
|
||||
border-color: var(--text-highlight-color, #49b1f5);
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.ts-result-title {
|
||||
font-weight: bold;
|
||||
color: var(--text-highlight-color, #49b1f5);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.ts-result-content {
|
||||
font-size: 0.9rem;
|
||||
color: var(--font-color, #666);
|
||||
line-height: 1.6;
|
||||
opacity: 0.85;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ts-result-item mark {
|
||||
background: #ffeb3b;
|
||||
color: #000;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.ts-pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.ais-Pagination-list {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
gap: 5px;
|
||||
}
|
||||
.ais-Pagination-link {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
color: var(--font-color, #333);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
background: var(--card-bg, #fff);
|
||||
}
|
||||
.ais-Pagination-link:hover {
|
||||
background: var(--text-highlight-color, #49b1f5);
|
||||
color: #fff;
|
||||
}
|
||||
.ais-Pagination-item--selected .ais-Pagination-link {
|
||||
background: var(--text-highlight-color, #49b1f5);
|
||||
color: #fff;
|
||||
}
|
||||
.ts-footer {
|
||||
text-align: right;
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid var(--border-color, #eee);
|
||||
padding-top: 10px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.ts-container {
|
||||
margin: 10px;
|
||||
width: calc(100% - 20px);
|
||||
padding: 20px 15px;
|
||||
}
|
||||
.ts-hits { max-height: 50vh; }
|
||||
}
|
||||
[data-theme="dark"] .ts-mask,
|
||||
.dark-mode .ts-mask {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', searchHTML);
|
||||
|
||||
const mask = document.getElementById('typesense-search-mask');
|
||||
const closeBtn = document.getElementById('close-typesense');
|
||||
const container = document.getElementById('typesense-search-container');
|
||||
|
||||
// ============================================================================
|
||||
// 搜索控制
|
||||
// ============================================================================
|
||||
function openSearch() {
|
||||
if (isSearchOpen) return;
|
||||
isSearchOpen = true;
|
||||
mask.style.display = 'block';
|
||||
void mask.offsetWidth;
|
||||
mask.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
if (!isInitialized) {
|
||||
initTypesense();
|
||||
}
|
||||
focusSearchInput();
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
if (!isSearchOpen) return;
|
||||
isSearchOpen = false;
|
||||
mask.classList.remove('active');
|
||||
setTimeout(function() {
|
||||
mask.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}, CONFIG.ui.animationDuration);
|
||||
}
|
||||
|
||||
function focusSearchInput(retryCount) {
|
||||
retryCount = retryCount || 0;
|
||||
const input = document.querySelector('.ais-SearchBox-input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.select();
|
||||
} else if (retryCount < CONFIG.ui.maxRetries) {
|
||||
setTimeout(function() {
|
||||
focusSearchInput(retryCount + 1);
|
||||
}, CONFIG.ui.retryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 事件监听
|
||||
// ============================================================================
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.search-typesense-trigger')) {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('click', closeSearch);
|
||||
mask.addEventListener('click', function(e) {
|
||||
if (e.target === mask) closeSearch();
|
||||
});
|
||||
container.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
window.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && isSearchOpen) {
|
||||
closeSearch();
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Typesense 初始化(带重试限制)
|
||||
// ============================================================================
|
||||
function initTypesense() {
|
||||
if (isInitialized || searchInstance) {
|
||||
console.warn('Typesense 搜索已初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
var instantsearchLoaded = typeof instantsearch !== 'undefined';
|
||||
var adapterLoaded = typeof TypesenseInstantSearchAdapter !== 'undefined' ||
|
||||
typeof window.TypesenseInstantSearchAdapter !== 'undefined';
|
||||
|
||||
console.log('📦 依赖库检查 (' + (initRetryCount + 1) + '/' + MAX_INIT_RETRIES + '):');
|
||||
console.log(' instantsearch.js:', instantsearchLoaded ? '✅ 已加载' : '❌ 未加载');
|
||||
console.log(' TypesenseAdapter:', adapterLoaded ? '✅ 已加载' : '❌ 未加载');
|
||||
|
||||
if (!instantsearchLoaded || !adapterLoaded) {
|
||||
initRetryCount++;
|
||||
|
||||
if (initRetryCount >= MAX_INIT_RETRIES) {
|
||||
console.error('');
|
||||
console.error('❌ Typesense 依赖库加载失败!');
|
||||
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.error('');
|
||||
console.error('🔧 请检查 _config.butterfly.yml 配置:');
|
||||
console.error('');
|
||||
console.error('inject:');
|
||||
console.error(' bottom: # ⚠️ 使用 bottom 而不是 head');
|
||||
console.error(' - <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.0"></script>');
|
||||
console.error(' - <script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>');
|
||||
console.error(' - <script src="/js/typesense-search-fixed.js"></script>');
|
||||
console.error('');
|
||||
console.error('💡 或在控制台手动检查:');
|
||||
console.error(' typeof instantsearch');
|
||||
console.error(' typeof TypesenseInstantSearchAdapter');
|
||||
console.error('');
|
||||
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
showErrorMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('⏳ 100ms 后重试...');
|
||||
setTimeout(initTypesense, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
initRetryCount = 0;
|
||||
console.log('🚀 开始初始化 Typesense...');
|
||||
|
||||
try {
|
||||
const typesenseAdapter = new TypesenseInstantSearchAdapter({
|
||||
server: {
|
||||
apiKey: CONFIG.apiKey,
|
||||
nodes: [{
|
||||
host: CONFIG.server.host,
|
||||
port: CONFIG.server.port,
|
||||
protocol: CONFIG.server.protocol
|
||||
}],
|
||||
cacheSearchResultsForSeconds: 120
|
||||
},
|
||||
additionalSearchParameters: CONFIG.searchParams
|
||||
});
|
||||
|
||||
searchInstance = instantsearch({
|
||||
searchClient: typesenseAdapter.searchClient,
|
||||
indexName: CONFIG.indexName,
|
||||
routing: false
|
||||
});
|
||||
|
||||
searchInstance.addWidgets([
|
||||
instantsearch.widgets.searchBox({
|
||||
container: '#searchbox',
|
||||
placeholder: '输入关键词寻找故事...',
|
||||
autofocus: true,
|
||||
showReset: true,
|
||||
showSubmit: false,
|
||||
showLoadingIndicator: true
|
||||
}),
|
||||
instantsearch.widgets.stats({
|
||||
container: '#stats',
|
||||
templates: {
|
||||
text: function(data) {
|
||||
if (!data.query) return '';
|
||||
return '找到 <strong>' + data.nbHits + '</strong> 条结果 (' + data.processingTimeMS + 'ms)';
|
||||
}
|
||||
}
|
||||
}),
|
||||
instantsearch.widgets.hits({
|
||||
container: '#hits',
|
||||
templates: {
|
||||
empty: function(results) {
|
||||
return '<div class="ts-empty">' +
|
||||
'<div><i class="fas fa-search"></i></div>' +
|
||||
'<div>找不到与 "<strong>' + results.query + '</strong>" 相关的内容</div>' +
|
||||
'<div style="margin-top: 10px;">试试其他关键词吧 (´·ω·`)</div>' +
|
||||
'</div>';
|
||||
},
|
||||
item: function(hit) {
|
||||
// 使用 _highlightResult 获取高亮文本
|
||||
var titleHighlight = hit._highlightResult && hit._highlightResult.title
|
||||
? hit._highlightResult.title.value
|
||||
: (hit.title || '');
|
||||
|
||||
var contentHighlight = hit._highlightResult && hit._highlightResult.content
|
||||
? hit._highlightResult.content.value
|
||||
: (hit.content || '');
|
||||
|
||||
// 截取内容长度
|
||||
if (contentHighlight.length > 200) {
|
||||
contentHighlight = contentHighlight.substring(0, 200) + '...';
|
||||
}
|
||||
|
||||
return '<a href="' + hit.url + '" class="ts-result-item">' +
|
||||
'<div class="ts-result-title">' + titleHighlight + '</div>' +
|
||||
'<div class="ts-result-content">' + contentHighlight + '</div>' +
|
||||
'</a>';
|
||||
}
|
||||
}
|
||||
}),
|
||||
instantsearch.widgets.pagination({
|
||||
container: '#pagination',
|
||||
padding: 2,
|
||||
showFirst: false,
|
||||
showLast: false
|
||||
})
|
||||
]);
|
||||
|
||||
searchInstance.start();
|
||||
isInitialized = true;
|
||||
|
||||
console.log('✅ Typesense 初始化成功!');
|
||||
|
||||
searchInstance.on('render', function() {
|
||||
if (isSearchOpen) {
|
||||
const input = document.querySelector('.ais-SearchBox-input');
|
||||
if (input && document.activeElement !== input) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化失败:', error);
|
||||
showErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 初始化
|
||||
// ============================================================================
|
||||
function init() {
|
||||
console.log('🔍 Typesense 搜索已准备就绪');
|
||||
console.log('💡 快捷键: Ctrl/Cmd + K');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 全局接口
|
||||
// ============================================================================
|
||||
window.TypesenseSearch = {
|
||||
open: openSearch,
|
||||
close: closeSearch,
|
||||
isOpen: function() { return isSearchOpen; },
|
||||
getInstance: function() { return searchInstance; }
|
||||
};
|
||||
|
||||
})();
|
||||
```
|
||||
|
||||
# 注意
|
||||
|
||||
{% note red %}
|
||||
indexName: "blogs" 和 collectionName: 'posts' 要一致!!!
|
||||
{% endnote %}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: 新年快乐!
|
||||
summary: >-
|
||||
这篇文章是一则关于新年祝福和幸福指数提升的通知,主要内容包含以下几点:首先,强调在假期期间要确保饮食质量,避免emo情绪的影响;其次,鼓励大家在收到通知后在留言区分享新年愿望,以便后台系统收集并祝愿他们实现愿望;最后,表达了对大家新年的美好祝愿,希望他们身体健康,万事如意,所得皆所愿,所行化坦途。
|
||||
tags:
|
||||
- 生活
|
||||
abbrlink: 5785bd01
|
||||
date: 2026-02-14 06:56:18
|
||||
categories:
|
||||
series:
|
||||
---
|
||||
|
||||
各位亲朋好友、合作伙伴及屏幕前的你:
|
||||
|
||||
值此二〇二六年蛇年来临之际,为贯彻落实“快乐至上”的核心价值观,进一步提升全体人员的幸福指数,现将有关事项通知如下:
|
||||
|
||||
一、各单位要切实做好“吃好喝好”保障工作,严禁在假期期间进行任何形式的emo。
|
||||
|
||||
二、请各有关人员在收到本通知后,务必在下方留言区留下你的新年愿望,由后台系统统一收集并祝愿其实现。
|
||||
|
||||
三、祝大家在新的一年里,身体健康,万事如意,所得皆所愿,所行化坦途!
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 自建bitwarden服务
|
||||
categories:
|
||||
- 技术
|
||||
cover: https://pic.biss.click/image/deaddd0c-e808-45f3-8354-3c44b2b65167.webp
|
||||
abbrlink: 7baa41fc
|
||||
summary: >-
|
||||
这篇文章介绍了如何自建Bitwarden服务,以替代浏览器自带的密码管理器。Bitwarden是一款开源、端到端加密的密码管理器,支持多种操作系统和设备,具有安全存储、自动同步、一键填充、生成随机密码和检测弱密码等功能。文章详细描述了部署Bitwarden服务的步骤,包括安装Vaultwarden、配置Nginx反向代理以及设置域名和数据卷等。通过这些步骤,用户可以轻松搭建一个功能强大的密码管理服务器,提高个人信息安全。
|
||||
date: 2026-02-25 11:09:23
|
||||
---
|
||||
|
||||
Bitwarden 是一款开源、端到端加密的密码管理器,支持 Windows、macOS、Linux、Android、iOS 以及几乎所有主流浏览器。
|
||||
它可以帮你:
|
||||
|
||||
- 安全存储密码、银行卡、笔记、密钥等敏感信息
|
||||
- 全设备自动同步
|
||||
- 一键自动填充账号密码
|
||||
- 生成高强度随机密码
|
||||
- 检测弱密码、重复密码、泄露密码
|
||||
|
||||
之前一直使用浏览器自带的密码管理器,跨平台不太好用,所以自建一个Bitwarden服务器。
|
||||
但是我们一般用Vaultwarden,占用更小,有一些Bitwarden的商业功能。
|
||||
|
||||
```yaml
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DOMAIN: "https://vw.domain.tld" #更改成自己的域名
|
||||
volumes:
|
||||
- ./vw-data/:/data/
|
||||
ports:
|
||||
- 127.0.0.1:8000:80
|
||||
|
||||
```
|
||||
|
||||
然后使用nignx进行反向代理,然后就可以尽情享用了。
|
||||
|
||||
{% link vaultwarden,vaultwarden,https://github.com/dani-garcia/vaultwarden %}
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: 安装gitea
|
||||
cover: https://pic.biss.click/image/961bc881-cb0a-4ab7-ace5-9990e71c30a0.webp
|
||||
categories: 技术
|
||||
tags: gitea
|
||||
abbrlink: 34725d47
|
||||
swiper_index: 1
|
||||
summary: >-
|
||||
这篇文章详细介绍了如何在2C2G的服务器上安装Gitea,包括获取二进制文件、创建用户和工作目录、创建系统服务、创建数据库以及安装Runner等步骤。Gitea作为一个轻量级的自助Git服务,适合用于托管网站源码。文章还提供了关于如何配置反向代理和数据库的信息,以及如何使用Docker运行Gitea
|
||||
Runner以实现GitHub Actions功能。通过这些步骤,用户可以在自己的服务器上成功部署Gitea,并利用其进行版本控制和自动化工作流程。
|
||||
date: 2026-02-07 06:32:04
|
||||
series:
|
||||
---
|
||||
|
||||
今天想把网站的源码转移到自建git仓,所以先来安装gitea吧(gitlab过于庞大,服务器配置不够)
|
||||
PS:我的服务器为2C2G
|
||||
|
||||
# 安装gitea
|
||||
这里用二进制文件安装
|
||||
## 获取二进制文件:
|
||||
```bash
|
||||
wget -O gitea https://dl.gitea.com/gitea/1.25.4/gitea-1.25.4-linux-amd64
|
||||
chmod +x gitea
|
||||
cp gitea /usr/local/bin/gitea
|
||||
```
|
||||
|
||||
## 创建用户
|
||||
这一步不是必须的,但是推荐这样,用root用户很容易出问题。
|
||||
|
||||
```bash
|
||||
# On Ubuntu/Debian:
|
||||
adduser \
|
||||
--system \
|
||||
--shell /bin/bash \
|
||||
--gecos 'Git Version Control' \
|
||||
--group \
|
||||
--disabled-password \
|
||||
--home /home/git \
|
||||
git
|
||||
|
||||
# On Fedora/RHEL/CentOS:
|
||||
groupadd --system git
|
||||
adduser \
|
||||
--system \
|
||||
--shell /bin/bash \
|
||||
--comment 'Git Version Control' \
|
||||
--gid git \
|
||||
--home-dir /home/git \
|
||||
--create-home \
|
||||
git
|
||||
```
|
||||
|
||||
## 创建工作目录
|
||||
|
||||
```bash
|
||||
mkdir -p /var/lib/gitea/{custom,data,log}
|
||||
chown -R git:git /var/lib/gitea/
|
||||
chmod -R 750 /var/lib/gitea/
|
||||
mkdir /etc/gitea
|
||||
chown root:git /etc/gitea
|
||||
chmod 770 /etc/gitea
|
||||
chmod 750 /etc/gitea
|
||||
chmod 640 /etc/gitea/app.ini
|
||||
```
|
||||
|
||||
## 创建系统服务
|
||||
|
||||
直接把github上面的挪过来就可以
|
||||
|
||||
{% link service文件,github,https://github.com/go-gitea/gitea/blob/release/v1.25/contrib/systemd/gitea.service %}
|
||||
|
||||
然后注册服务并启动
|
||||
```bash
|
||||
sudo systemctl enable gitea
|
||||
sudo systemctl start gitea
|
||||
```
|
||||
|
||||
## 创建数据库
|
||||
可以用MySQL数据库或者PostgreSQL,创建一个数据库在web页面填写进去就行。
|
||||
|
||||
反向代理略过,和普通网站的反向代理配置没有什么区别。
|
||||
|
||||
# 安装runner
|
||||
这个runner也不是必须的,是为了实现github的action功能;在2C2G服务器上我看运行的还可以,当然,只是这个hexo博客的自动构建,占用资源也少;
|
||||
使用doker,这也是官方建议。以下是compose文件:
|
||||
|
||||
```yaml
|
||||
|
||||
services:
|
||||
runner:
|
||||
image: gitea/act_runner:latest
|
||||
ports:
|
||||
- 8088:8088
|
||||
environment:
|
||||
- CONFIG_FILE=/config.yaml
|
||||
- GITEA_INSTANCE_URL=https://git.biss.click
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN= #替换成自己的token
|
||||
volumes:
|
||||
- ./config.yaml:/config.yaml
|
||||
- ./data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock # 允许 Runner 调用宿主机 Docker
|
||||
|
||||
```
|
||||
token在管理后台 工作流-运行器-新建运行器获取
|
||||
config文件需要这样生成
|
||||
|
||||
```bash
|
||||
docker run --entrypoint="" --rm -it docker.io/gitea/act_runner:latest act_runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
在后台工作流运行器可以看见就没问题了。
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: 将博客仓库转移到gitea
|
||||
cover: https://pic.biss.click/image/f9767ecf-b8de-461b-8e62-8f7444297ea6.webp
|
||||
categories: 技术
|
||||
tags: gitea
|
||||
abbrlink: d2c8521
|
||||
summary: >-
|
||||
这篇文章详细介绍了如何将博客仓库从现有的平台转移到Gitea。首先,确保已经成功安装了Gitea。接着,文章重点讲解了action文件的修改,包括检查分支、缓存项目、安装Node和Hexo、安装依赖、清理文件树、生成静态文件并压缩、部署到本地仓库以及Webhook的配置。这些步骤确保了博客能够顺利迁移到Gitea,并且保持了原有的功能和结构。需要注意的是,文章中的action文件包含了一些具体的指令和变量,如ref:
|
||||
master、cache-node-modules等,这些在实际操作时需要替换为具体的值。此外,文章还提到了Webhook的配置,这是一个可选但推荐的自定义事件触发器,用于在特定事件发生时自动触发部署流程。
|
||||
date: 2026-02-07 12:30:39
|
||||
series:
|
||||
---
|
||||
|
||||
在上一篇文章中已经完成了gitea的安装
|
||||
那么博客源码迁移倒是没问题,直接```git remote add origin```就行,但是action文件就有些变更。
|
||||
这是我修改的action文件:
|
||||
|
||||
```yaml
|
||||
name: 自动部署
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
env:
|
||||
TZ: Asia/Shanghai
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检查分支
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
- name: 缓存项目 npm 包
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-nodeModules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nodeModules-
|
||||
- name: 安装 Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
- name: 安装 Hexo
|
||||
run: |
|
||||
npm install hexo-cli --global
|
||||
- name: 安装依赖
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
npm install
|
||||
- name: 清理文件树
|
||||
run: |
|
||||
npm run clean
|
||||
- name: 生成静态文件并压缩
|
||||
run: |
|
||||
npm run build
|
||||
- name: 部署
|
||||
run: |
|
||||
cd ./public
|
||||
git init
|
||||
git config user.name "${{ gitea.actor }}"
|
||||
git config user.email "${{ gitea.actor }}@noreply.gitea.io"
|
||||
git add .
|
||||
git commit -m "${{ gitea.event.head_commit.message }}··[$(date +"%Z %Y-%m-%d %A %H:%M:%S")]"
|
||||
git push --force --quiet "https://${{ gitea.actor }}:${{ secrets.DEPLOY_TOKEN }}@git.biss.click/biss/blog.git" master:page
|
||||
- name: Deploy to Server
|
||||
run: |
|
||||
curl -k -X POST
|
||||
```
|
||||
仅供参考吧,最后面是webhook,可以自己改改。
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
title: opencv应用-基础操作
|
||||
categories:
|
||||
- 技术
|
||||
series: opencv
|
||||
tags:
|
||||
- opencv
|
||||
abbrlink: e8f95ead
|
||||
summary: >-
|
||||
这篇文章介绍了使用C++和OpenCV库进行图像处理的基础操作,包括图片的读取与展示、像素的读取与修改、图像的基本变换(缩放、旋转、平移、翻转)、以及图像通道的分离与合并。文章详细讲解了每个操作的代码实现,并展示了如何通过OpenCV库提供的函数来完成这些任务。这对于初学者来说是非常有用的,因为它帮助他们理解如何使用OpenCV进行图像处理。
|
||||
date: 2026-03-15 09:56:51
|
||||
---
|
||||
|
||||
这里用C++进行编程,发现菜鸟教程只有python的版本,那就记录一下。
|
||||
|
||||
# 图片读取与展示
|
||||
|
||||
```cpp
|
||||
// 读取图像
|
||||
#include<opencv2/opencv.hpp>
|
||||
#include<iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
int main(){
|
||||
Mat src = imread("../img/1.png");
|
||||
imshow("input",src);
|
||||
waitKey(0);
|
||||
destroyAllWindows();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
# 图像基本操作
|
||||
## 读取像素
|
||||
需要用到三维向量数组Vect3b,这里需要注意的是,Opencv是BGR而不是我们常用的RGB。
|
||||
```cpp
|
||||
// 读取像素
|
||||
#include<opencv2/opencv.hpp>
|
||||
#include<iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
int main()
|
||||
{
|
||||
string image_path = "../img/1.png";
|
||||
Mat image = imread(image_path);
|
||||
|
||||
if (image.empty()) {
|
||||
cout << "错误:无法加载图像,请检查路径是否正确。" << endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Vec3b pixel_value = image.at<Vec3b>(100, 150);
|
||||
|
||||
cout << "B: " << (int)pixel_value[0] << " "
|
||||
<< "G: " << (int)pixel_value[1] << " "
|
||||
<< "R: " << (int)pixel_value[2] << endl;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 修改像素
|
||||
|
||||
```cpp
|
||||
// 修改像素
|
||||
#include<opencv2/opencv.hpp>
|
||||
#include<iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
int main()
|
||||
{
|
||||
string image_path = "../img/1.png";
|
||||
Mat image = imread(image_path);
|
||||
Mat result = image.clone();
|
||||
|
||||
if (image.empty()) {
|
||||
cout << "错误:无法加载图像,请检查路径是否正确。" << endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Rect roi_rect(0, 0, 100, 100);
|
||||
Mat roi = result(roi_rect);
|
||||
roi.setTo(Scalar(0, 255, 0));
|
||||
imshow("Original (Unchanged)", image);
|
||||
imshow("Modified Copy", result);
|
||||
waitKey(0);
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
// 读取图像
|
||||
cv::Mat img = cv::imread("../img/1.png");
|
||||
if (img.empty()) {
|
||||
std::cout << "无法读取图像" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 1. 缩放
|
||||
cv::Mat resized_img;
|
||||
cv::resize(img, resized_img, cv::Size(200, 200));
|
||||
cv::imshow("resized_img", resized_img);
|
||||
|
||||
// 2. 旋转
|
||||
cv::Mat rotated_img, M_rot;
|
||||
cv::Point2f center(img.cols / 2.0, img.rows / 2.0);
|
||||
M_rot = cv::getRotationMatrix2D(center, 45, 1.0);
|
||||
cv::warpAffine(img, rotated_img, M_rot, img.size());
|
||||
cv::imshow("rotated_img", rotated_img);
|
||||
|
||||
// 3. 平移
|
||||
cv::Mat translated_img;
|
||||
cv::Mat M_trans = (cv::Mat_<float>(2, 3) << 1, 0, 100, 0, 1, 50);
|
||||
cv::warpAffine(img, translated_img, M_trans, img.size());
|
||||
cv::imshow("translated_img", translated_img);
|
||||
|
||||
// 4. 翻转
|
||||
cv::Mat flipped_img;
|
||||
cv::flip(img, flipped_img, 1);
|
||||
cv::imshow("flipped", flipped_img);
|
||||
|
||||
// 显示结果
|
||||
cv::imshow("Original", img);
|
||||
cv::waitKey(0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// 图像通道分离与合并
|
||||
#include<opencv2/opencv.hpp>
|
||||
#include<iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
int main()
|
||||
{
|
||||
string image_path = "../img/1.png";
|
||||
Mat image = imread(image_path);
|
||||
Mat result = image.clone();
|
||||
// 定义向量数组接收通道
|
||||
vector<Mat> channels;
|
||||
// 拆分
|
||||
split(result,channels);
|
||||
|
||||
Mat b = channels[0];
|
||||
Mat g = channels[1];
|
||||
Mat r = channels[2];
|
||||
|
||||
imshow("Blue Channel (Grayscale)", channels[0]);
|
||||
imshow("Green Channel (Grayscale)", channels[1]);
|
||||
imshow("Red Channel (Grayscale)", channels[2]);
|
||||
|
||||
// merge
|
||||
Mat merged_img;
|
||||
vector<cv::Mat> channels_to_merge;
|
||||
channels_to_merge.push_back(b);
|
||||
channels_to_merge.push_back(g);
|
||||
channels_to_merge.push_back(r);
|
||||
|
||||
merge(channels_to_merge, merged_img);
|
||||
imshow("merged",merged_img);
|
||||
|
||||
waitKey(0);
|
||||
|
||||
return(0);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: opencv应用-算术运算
|
||||
categories:
|
||||
- 技术
|
||||
series: opencv
|
||||
tags:
|
||||
- opencv
|
||||
abbrlink: b559997d
|
||||
summary: >-
|
||||
这篇文章介绍了如何使用OpenCV进行图像的加法运算。首先,通过包含OpenCV和iostream头文件,并使用std命名空间,开始编写代码。主函数中,读取两张图片,检查是否成功加载,并确保它们的尺寸相同。如果尺寸不同,将第二张图片调整为与第一张相同的大小。然后,调用add函数对两幅图片进行加法运算,并将结果存储在result矩阵中。最后,显示原始图片和加法运算的结果,并等待按键关闭窗口。代码已上传至自建的git仓库。
|
||||
date: 2026-03-17 13:13:08
|
||||
---
|
||||
|
||||
opencv对图像的算数运算,感觉都大同小异,分为以下几种:加减乘除和位运算。
|
||||
|
||||
| 函数 | 功能 | 应用场景 |
|
||||
|-------------------|--------|------------|
|
||||
| cv2.bitwise_and() | 按位与操作 | 掩码操作、图像分割 |
|
||||
| cv2.bitwise_or() | 按位或操作 | 图像叠加 |
|
||||
| cv2.bitwise_not() | 按位取反操作 | 图像反色 |
|
||||
| cv2.bitwise_xor() | 按位异或操作 | 图像差异检测 |
|
||||
|
||||
因为感觉都差不多,所以只把加法运算代码搬过来
|
||||
|
||||
```cpp
|
||||
// 加法
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
using namespace cv;
|
||||
|
||||
int main()
|
||||
{
|
||||
string path1 = "../img/2.jpg";
|
||||
string path2 = "../img/3.jpg";
|
||||
|
||||
Mat img1 = imread(path1);
|
||||
Mat img2 = imread(path2);
|
||||
|
||||
if (img1.empty() || img2.empty())
|
||||
{
|
||||
cout << "无法读取图片,请检查路径!" << endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (img1.size() != img2.size())
|
||||
{
|
||||
cout << "尺寸不一致,正在自动调整 img2 的尺寸..." << endl;
|
||||
// 将 img2 缩放到与 img1 相同的尺寸
|
||||
resize(img2, img2, img1.size());
|
||||
}
|
||||
|
||||
Mat result;
|
||||
add(img1, img2, result);
|
||||
|
||||
imshow("Original Image 1", img1);
|
||||
imshow("Original Image 2", img2);
|
||||
imshow("Add Result", result);
|
||||
|
||||
// 等待按键后关闭窗口
|
||||
waitKey(0);
|
||||
destroyAllWindows();
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
另外,把代码上传到了自建的git中
|
||||
{% link opencv-learning,opencv-learning,https://git.biss.click/biss/opencv-learning/src/branch/master/Arithmetic%20operations %}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: 配置opencv
|
||||
cover: https://pic.biss.click/image/c177a2e2-63af-4582-98b6-e95726f6cdc5.webp
|
||||
categories:
|
||||
- 技术
|
||||
series: opencv
|
||||
tags: opencv
|
||||
abbrlink: 6e3332b
|
||||
summary: >-
|
||||
这篇文章介绍了在Ubuntu和Windows系统上配置OpenCV的过程。在Ubuntu系统中,通过包管理器安装了Visual
|
||||
Studio、GCC、G++、OpenCV以及Python的OpenCV绑定。而在Windows系统中,则通过安装Visual
|
||||
Studio并下载OpenCV的Windows安装包进行配置,包括将安装路径添加到系统环境变量中。
|
||||
date: 2026-03-14 11:48:36
|
||||
---
|
||||
|
||||
# 引言
|
||||
不知道为什么我们专业看起来与计算机八竿子打不着,竟然要学`opencv`,那就来记录一下吧!
|
||||
# Ubuntu 篇
|
||||
|
||||
`Ubuntu`因为有完善的包管理体系,所以配环境相对简单。
|
||||
|
||||
先安装`vscode`,当然也可以不安装,只是个编辑器;
|
||||
{% link vscode,Microsoft,https://code.visualstudio.com/ %}
|
||||
到这里下载`deb`格式的软件包,然后`dpkg`安装即可。可以安装这些扩展:
|
||||
|
||||
<center><img src=https://pic.biss.click/image/3369ff6a-3286-4ccd-b261-a38802f7d9f2.webp)></center>
|
||||
|
||||
接下来安装编译器
|
||||
|
||||
```bash
|
||||
# 先更新软件包
|
||||
apt update
|
||||
apt upgrade -y
|
||||
# 安装C/C++编译器
|
||||
apt install gcc g++
|
||||
# 安装opencv
|
||||
apt install -y mesa-utils
|
||||
apt install -y libopencv-dev
|
||||
apt install -y opencv
|
||||
apt install -y python3-opencv #Python opencv
|
||||
```
|
||||
然后就可以了。
|
||||
|
||||
# Windows 篇
|
||||
`Windows` 下需要安装`Visual Studio`,比较简单,官网下载安装就行。选择使用C++桌面开发就行。
|
||||
下载`opencv`,在下边网站下载`Windows`安装包,
|
||||
{% link opencv,opencv,https://opencv.org/releases/ %}
|
||||
下载后是一个自解压包,解压路径设置成自己想要的路径,
|
||||
把安装路径添加到系统环境变量中,例如`D:\Opencv\opencv\build\x64\vc16\bin`
|
||||
然后应该就没问题了。
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: GPT Image2 初试
|
||||
cover: 'https://pic.biss.click/image/92de2106-ee94-493e-ad40-b7d4054b23c6.webp'
|
||||
categories:
|
||||
- 技术
|
||||
tags:
|
||||
- OpenAI
|
||||
- AI
|
||||
- 图像生成
|
||||
summary: >-
|
||||
这篇文章介绍了作者最近试用gpt
|
||||
img2之后的一些直观感受。文章没有从技术角度展开,而是以普通用户的体验为主,聊了它适合做什么、哪些地方比以前的AI绘图更顺手、哪些地方依然不太稳定,以及在日常做头像、壁纸、封面图和改图时的一些实际体会。整体来看,它确实比以前更听话一些,但也还没有到可以完全省心的地步。
|
||||
abbrlink: 3f8de22
|
||||
date: 2026-04-25 15:00:00
|
||||
---
|
||||
|
||||
最近看到不少人在说 `gpt img2` ,我也去试了试。
|
||||
先说结论,确实比我以前随手玩过的一些 AI 画图工具要顺手,尤其是在“听懂人话”这件事上,感觉明显好了不少。
|
||||
我生成了几张图:
|
||||

|
||||

|
||||
不过它也没有神到那种地步,不是说你脑子里想什么,它就百分百给你画什么。更多时候,我会觉得它像一个比较聪明、但偶尔还是会跑偏的画图助手。
|
||||
|
||||
# 我为什么会去试这个
|
||||
|
||||
其实我平时并不是很重度用 AI 画图的人,最多也就是做这几种东西:
|
||||
|
||||
- 文章封面图
|
||||
- 头像
|
||||
- 壁纸
|
||||
- 一些网页装饰用的小图
|
||||
- 改改现成图片
|
||||
|
||||
以前我对这类工具最大的感受就是,能画,但是不稳定。
|
||||
有时候第一张就很好,有时候连续改十几次都不对味。尤其是你要求稍微细一点,它就很容易开始自由发挥。
|
||||
|
||||
这次去试 `gpt img2` ,我本来也是抱着“看看是不是又换了个名字继续宣传”的想法,结果实际用下来,至少第一印象还可以。
|
||||
|
||||
# 第一感觉就是更听话了
|
||||
|
||||
这个是我最直接的感受。
|
||||
|
||||
以前很多 AI 画图工具,你写一大段描述,它看起来像是认真读了,但实际生成出来,总有一种“只挑了几句重点听”的感觉。
|
||||
比如你明明说的是干净背景、淡色、安静一点的氛围,它最后给你来一张花里胡哨的图。
|
||||
|
||||
`gpt img2` 相对来说,会更愿意按照你写的意思去靠。
|
||||
|
||||
当然,也不是说它就完全不会乱来,而是那种“离题太远”的情况少了一点。
|
||||
普通用户用的时候,这个差别其实挺明显的,因为很多时候我们并不想研究什么复杂提示词,只想把自己脑子里的画面尽量说清楚。
|
||||
|
||||
# 比较适合我这种普通人的地方
|
||||
|
||||
我自己觉得,它最适合的不是那种特别专业的商业设计,而是下面这些日常场景:
|
||||
|
||||
## 做头像和壁纸
|
||||
|
||||
这个是最轻松的。
|
||||
|
||||
你只要把风格、颜色、主体大概说一下,一般都能出个八九不离十的结果。
|
||||
比如想要一个偏安静、偏插画风的头像,或者想做一个简洁一点的手机壁纸,这种它完成得还不错。
|
||||
|
||||
而且有一个很现实的问题,头像和壁纸对“绝对精确”要求没那么高,所以就算它有一点点小偏差,也不太影响使用。
|
||||
|
||||
## 做文章封面图
|
||||
|
||||
这个我感觉也挺适合。
|
||||
|
||||
尤其是写博客的时候,有时候正文已经写完了,但就是不想花太多时间去找图。
|
||||
这个时候让它给一张气质差不多的配图,其实很省事。
|
||||
|
||||
不过文章封面图有个问题,就是如果你想让它在图里放很准确的中文文字,那还是不要太指望。
|
||||
拿来做氛围图可以,拿来直接当完整海报,我觉得还差一点意思。
|
||||
|
||||
## 改图比纯出图更有意思
|
||||
|
||||
这个是我试完之后觉得比较有趣的一点。
|
||||
|
||||
以前我对 AI 画图更多是“从零生成”,但这次明显感觉“改现成图”会更实用一些。
|
||||
比如:
|
||||
|
||||
- 背景不喜欢,换一下
|
||||
- 色调不喜欢,柔和一点
|
||||
- 构图太满了,简洁一点
|
||||
- 想保留主体,只改周围环境
|
||||
|
||||
这种比从头重新生成方便很多。
|
||||
因为你本来就已经有一个大致方向了,只是想在它基础上继续修。
|
||||
|
||||
# 它并不是每次都稳定
|
||||
|
||||
虽然前面夸了不少,但还是得说,AI 画图这东西目前依然带一点玄学。
|
||||
|
||||
有时候第一张就很顺眼,甚至顺眼到让我怀疑“是不是今天运气太好了”。
|
||||
但有时候同样一句话,换一次结果就完全不是那个味。
|
||||
|
||||
所以如果一定要我给个很真实的评价,我会觉得:
|
||||
|
||||
它比以前更容易出到“能用的图”
|
||||
但还没有到“你完全不用挑图”的程度
|
||||
|
||||
这两个感觉差别还是挺大的。
|
||||
|
||||
# 文字还是不要太相信
|
||||
|
||||
这个基本算是我对所有 AI 出图工具的统一评价了。
|
||||
|
||||
如果只是画面里带一点很简单的小字,也许能凑合看看。
|
||||
但你要是想让它直接给你做一张字很多、排版还工整的海报,我感觉还是容易翻车。
|
||||
|
||||
尤其是中文,很多时候看着像那么回事,细看就不是很对。
|
||||
所以如果只是做背景图、插图、封面图,这个没什么问题。
|
||||
但如果是正式一点的图文海报,我还是更愿意后面自己补字。
|
||||
|
||||
# 最麻烦的还是“差一点点”
|
||||
|
||||
我觉得 AI 画图最折磨人的地方,从来都不是完全不行,而是它经常会给你一张“差一点点”的图。
|
||||
|
||||
比如:
|
||||
|
||||
- 整体很好,但手不对
|
||||
- 脸差不多,但眼神怪怪的
|
||||
- 背景挺好,但颜色太脏
|
||||
- 构图不错,但多出来几个没必要的东西
|
||||
|
||||
这种图你删了可惜,用了又不甘心。
|
||||
然后你就会进入一个“再改一下应该就好了吧”的循环。
|
||||
|
||||
所以我现在反而会提醒自己,别太贪。
|
||||
差不多能用的时候就停,不然改着改着,很容易把原来那张还不错的也改没了。
|
||||
|
||||
# 最后
|
||||
|
||||
反正我试下来,对 `gpt img2` 的感觉大概就是:
|
||||
|
||||
它不是那种会让我惊呼“以后再也不用别的了”的东西,
|
||||
但它确实是目前让我觉得“拿来日常做图已经挺方便了”的一类工具。
|
||||
|
||||
对于普通用户来说,我觉得这就够了。
|
||||
能帮我少折腾一点,少改几轮,偶尔还能出几张挺喜欢的图,那它就已经算好用了。
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: 配置OpenGL环境
|
||||
cover: https://pic.biss.click/image/d19b9e03-9442-4b85-94a0-a3780b9f4440.webp
|
||||
categories:
|
||||
- 技术
|
||||
- 学习
|
||||
series: OpenGL
|
||||
tags: OpenGL
|
||||
abbrlink: c4477b0c
|
||||
summary: >-
|
||||
这篇文章详细介绍了配置OpenGL环境所需步骤,包括安装Visual
|
||||
Studio、CMake和GLFW等关键组件,并提供了具体的配置方法和测试代码,旨在帮助读者顺利开始计算机图形学的学习之旅。
|
||||
date: 2026-04-02 04:52:16
|
||||
---
|
||||
|
||||
最近要学计算机图形学,所以会用到`OpenGL`,配置环境有点繁琐,记录了下来。
|
||||
|
||||
# 安装Visual Studio
|
||||
现在我们先来安装`Visual Studio`:[visual studio](https://visualstudio.microsoft.com/zh-hans/vs/),下载后安装即可|在安装时选择“使用C++的桌面开发”,这样安装时就会安装C++的编译器了。
|
||||

|
||||
|
||||
# 安装 Cmake
|
||||
这个可选,因为我们可以使用`Visual Studio`的编译器`MSVC`来编译项目。
|
||||
`Cmake`是一个开源的跨平台软件构建工具,它可以生成不同系统的构建文件,比如`Makefile`,`Ninja`,`VS`项目文件等等。
|
||||
我们可以下载`Cmake`:[cmake](https://cmake.org/download/),下载后安装即可。
|
||||
|
||||
# 安装GLFW
|
||||
OpenGL有许多工具,比如GLFW,GLEW等等,这里我们安装GLFW。
|
||||
|工具|类别|主要职责|特点|
|
||||
|----|----|----|----|
|
||||
|GLUT|窗口管理 + 工具库|创建窗口、处理鼠标键盘、提供内置渲染循环。|古老、简单。使用“固定管线”(老旧技术),适合教学。|
|
||||
|GLFW|窗口管理库|创建窗口、处理输入、管理多个上下文。|现代、轻量。只管窗口和输入,不负责渲染逻辑,是目前的主流。|
|
||||
|GLAD|配置/加载库|加载 OpenGL 函数指针(连接驱动)。|底层必备。因为 OpenGL 函数在显卡驱动里,需要它来“找”函数地址。|
|
||||
|
||||
## 下载GLFW 和 glad
|
||||
`GLFW`是一个开源的跨平台`C/C++`窗口管理库,它可以创建窗口、处理输入、管理多个上下文。
|
||||
下载`GLFW`:[glfw](https://www.glfw.org/download.html),下载后解压即可。
|
||||
|
||||
|
||||
|
||||
## 创建项目与配置
|
||||
用`Visual Studio`创建一个空的`C++`项目,然后添加GLFW的目录到项目中。
|
||||
在项目属性中添加GLFW的目录到“C/C++->General->Additional Include Directories”中。
|
||||
如图所示:
|
||||

|
||||
|
||||
## 测试
|
||||
|
||||
创建一个`main.cpp`文件,内容如下:
|
||||
```cpp
|
||||
#include <glad/glad.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <iostream>
|
||||
|
||||
// 顶点着色器源码
|
||||
const char* vertexShaderSource = "#version 330 core\n"
|
||||
"layout (location = 0) in vec3 aPos;\n"
|
||||
"void main() {\n"
|
||||
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
|
||||
"}\0";
|
||||
|
||||
// 片段着色器源码
|
||||
const char* fragmentShaderSource = "#version 330 core\n"
|
||||
"out vec4 FragColor;\n"
|
||||
"void main() {\n"
|
||||
" FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);\n" // 白色
|
||||
"}\n\0";
|
||||
|
||||
int main() {
|
||||
// 1. 初始化 GLFW
|
||||
glfwInit();
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
|
||||
// 2. 创建窗口
|
||||
GLFWwindow* window = glfwCreateWindow(800, 600, "Simple OpenGL", NULL, NULL);
|
||||
glfwMakeContextCurrent(window);
|
||||
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
|
||||
|
||||
// 3. 编译着色器
|
||||
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
|
||||
glCompileShader(vertexShader);
|
||||
|
||||
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
|
||||
glCompileShader(fragmentShader);
|
||||
|
||||
unsigned int shaderProgram = glCreateProgram();
|
||||
glAttachShader(shaderProgram, vertexShader);
|
||||
glAttachShader(shaderProgram, fragmentShader);
|
||||
glLinkProgram(shaderProgram);
|
||||
|
||||
// 4. 定义三角形顶点数据
|
||||
float vertices[] = {
|
||||
-0.5f, -0.5f, 0.0f, // 左下
|
||||
0.5f, -0.5f, 0.0f, // 右下
|
||||
0.0f, 0.5f, 0.0f // 顶部
|
||||
};
|
||||
|
||||
unsigned int VBO, VAO;
|
||||
glGenVertexArrays(1, &VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
// 5. 渲染循环
|
||||
while (!glfwWindowShouldClose(window)) {
|
||||
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 深青色背景
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
glUseProgram(shaderProgram);
|
||||
glBindVertexArray(VAO);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
|
||||
glfwSwapBuffers(window);
|
||||
glfwPollEvents();
|
||||
}
|
||||
|
||||
glfwTerminate();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
如果能运行成功应该就没问题了。
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: 电子价签玩法
|
||||
categories: 技术
|
||||
abbrlink: 8712baaa
|
||||
summary: >-
|
||||
这篇文章介绍了作者在逛咸鱼时发现并购买了两个改造二手电子价签的过程,这些电子价签由tsl0922开发的开源固件提供支持,具备日历、传图、文字功能,并且可以在网页端进行便捷的配置。尽管开源版固件功能有限,作者选择自行编写所需功能,目前已经实现了待办事项和倒计时功能,并能生成图片后上传至网络。
|
||||
date: 2026-04-19 13:46:25
|
||||
series:
|
||||
tags:
|
||||
---
|
||||
|
||||
最近在逛咸鱼时发现了改造二手电子价签,实现了一点有意思的功能,于是买了两个玩玩。
|
||||
他们用的是tsl0922开发的开源固件,具有日历、传图、文字功能,在网页端可以很方便地配置。
|
||||
|
||||
{% link 上位机,tsl0922,https://tsl0922.github.io/EPD-nRF5/ %}
|
||||
|
||||

|
||||

|
||||
当然开源版固件功能有点少,又不想买烧录器自己折腾,所以把自己想要的功能写成网页:
|
||||
目前写了待办事项和倒计时,生成图片后上传
|
||||
{% link 生成图,tu,https://letters.biss.click/gen/ %}
|
||||

|
||||

|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: OpenGL-基础程序
|
||||
cover: https://pic.biss.click/image/2fcb9566-f7f6-4132-81cb-4cd646967519.webp
|
||||
categories:
|
||||
- 技术
|
||||
- 学习
|
||||
series: OpenGL
|
||||
tags: OpenGL
|
||||
abbrlink: 437a5198
|
||||
summary: >-
|
||||
这篇文章详细介绍了OpenGL的基础程序,包括代码展示和代码解释两部分。代码展示部分呈现了一个简单的OpenGL程序,涵盖了初始化与窗口管理、状态设置与缓冲区操作、矩阵与坐标变换以及几何图形绘制等关键步骤。代码解释则将这些步骤按照功能类别进行了归纳,帮助读者更好地理解OpenGL的渲染流程。通过这个示例,读者可以初步掌握OpenGL的基本操作和概念。
|
||||
date: 2026-04-11 18:20:34
|
||||
---
|
||||
|
||||
# 代码展示
|
||||
|
||||
我们先从基本的OpenGL程序开始吧,这是一个简单的OpenGL程序:
|
||||
|
||||
```cpp
|
||||
#include<GL/glut.h>
|
||||
using namespace std;
|
||||
|
||||
// 回调函数
|
||||
void myDisplay()
|
||||
{
|
||||
// 清除缓冲区
|
||||
glClearColor(0.0, 0.0, 0.0, 0.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// 正交模式
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
gluOrtho2D(0.0, 500.0, 0.0, 500.0);
|
||||
glColor4f(0.0, 1.0, 0.0, 0.0);
|
||||
glRectf(50.0, 50.0, 400.0, 400.0);
|
||||
|
||||
// 划线
|
||||
glColor3f(1.0, 1.0, 0.0);
|
||||
glBegin(GL_LINES);
|
||||
glVertex2f(50.0, 50.0);
|
||||
glVertex2f(400.0, 400.0);
|
||||
glVertex2f(400.0, 50.0);
|
||||
glVertex2f(50.0, 400.0);
|
||||
glEnd();
|
||||
|
||||
// 画点
|
||||
glColor3f(1.0, 0.0, 0.0);
|
||||
glPointSize(20.0);
|
||||
glBegin(GL_POINTS);
|
||||
glVertex2f(15.0, 15.0);
|
||||
glEnd();
|
||||
|
||||
// 画三角形
|
||||
glBegin(GL_TRIANGLES);
|
||||
glColor3f(0.0, 0.0, 1.0);
|
||||
glVertex2i(200, 300);
|
||||
glVertex2i(100, 100);
|
||||
glVertex2i(300, 100);
|
||||
glEnd();
|
||||
|
||||
|
||||
glFlush();
|
||||
|
||||
}
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
glutInit(&argc, argv);
|
||||
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
|
||||
glutInitWindowPosition(100, 100);
|
||||
glutInitWindowSize(500, 500);
|
||||
glutCreateWindow("");
|
||||
glutDisplayFunc(&myDisplay);
|
||||
glutMainLoop();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
这是运行结果图:
|
||||

|
||||
|
||||
# 代码解释
|
||||
这段代码展示了经典的 OpenGL (GLUT) 固定渲染管线基本操作。为了方便理解,我将这些函数按照 **初始化与窗口管理**、**状态设置**、**坐标变换** 以及 **图形绘制** 四个类别进行了整理。
|
||||
|
||||
## 1. 窗口管理与初始化 (GLUT 库)
|
||||
这类函数主要用于配置窗口系统和处理程序的运行流程。
|
||||
|
||||
| 函数名称 | 功能描述 | 核心参数说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `glutInit` | 初始化 GLUT 库 | 接收 `main` 函数的命令行参数 |
|
||||
| `glutInitDisplayMode` | 设置显示模式 | `GLUT_RGB` (使用颜色模式), `GLUT_SINGLE` (单缓冲区) |
|
||||
| `glutInitWindowPosition` | 设置窗口在屏幕上的初始位置 | 窗口左上角坐标 $(x, y)$ |
|
||||
| `glutInitWindowSize` | 设置窗口的宽度和高度 | 像素值 (Width, Height) |
|
||||
| `glutCreateWindow` | 创建窗口 | 字符串参数作为窗口标题 |
|
||||
| `glutDisplayFunc` | 注册显示回调函数 | 传入负责绘图的函数指针 |
|
||||
| `glutMainLoop` | 进入 GLUT 事件处理循环 | 让程序持续运行,等待重绘或交互 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 状态设置与缓冲区操作
|
||||
这些函数用于配置 OpenGL 的全局状态(如颜色、点大小)或清理画布。
|
||||
|
||||
| 函数名称 | 功能描述 | 核心参数说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `glClearColor` | 设置清除颜色(背景色) | RGBA 值 (0.0~1.0),此处设为黑色 |
|
||||
| `glClear` | 清除指定的缓冲区 | `GL_COLOR_BUFFER_BIT` 表示清除颜色缓存 |
|
||||
| `glColor4f` | 设置当前的绘制颜色 (带透明度) | RGBA 分量 |
|
||||
| `glColor3f` | 设置当前的绘制颜色 (不带透明度) | RGB 分量 |
|
||||
| `glPointSize` | 设置点的像素大小 | 浮点数,数值越大点越粗 |
|
||||
| `glFlush` | 强制刷新缓冲区 | 确保绘图命令立即执行并输出到显示设备 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 矩阵与坐标变换
|
||||
用于定义物体是如何投影到屏幕上的。
|
||||
|
||||
| 函数名称 | 功能描述 | 核心参数说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `glMatrixMode` | 设置当前矩阵模式 | `GL_PROJECTION` 切换到投影矩阵堆栈 |
|
||||
| `gluOrtho2D` | 定义二维正交投影裁剪区域 | 定义视野的左、右、下、上边界范围 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 几何图形绘制
|
||||
OpenGL 的核心绘图逻辑,通过指定顶点来构建形状。
|
||||
|
||||
| 函数名称 | 功能描述 | 核心参数说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `glRectf` | 绘制一个实心矩形 | 传入左下角坐标和右上角坐标 $(x1, y1, x2, y2)$ |
|
||||
| `glBegin` | 标记图元绘制的开始 | `GL_LINES` (线), `GL_POINTS` (点), `GL_TRIANGLES` (三角形) |
|
||||
| `glVertex2f` | 指定一个二维顶点 (浮点型) | 坐标 $(x, y)$ |
|
||||
| `glVertex2i` | 指定一个二维顶点 (整型) | 坐标 $(x, y)$ |
|
||||
| `glEnd` | 标记图元绘制的结束 | 必须与 `glBegin` 成对出现 |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
title: OpenGL-直线的扫描转换
|
||||
cover: https://pic.biss.click/image/c5457adc-214c-4a18-9aa6-43fbc0bfc2f4.webp
|
||||
categories:
|
||||
- 技术
|
||||
- 学习
|
||||
series: OpenGL
|
||||
tags: OpenGL
|
||||
abbrlink: 7207243b
|
||||
summary: >-
|
||||
这篇文章介绍了三种直线扫描转换算法:DDA算法、中点画线算法和Bresenham算法。
|
||||
DDA算法通过计算直线起点和终点的坐标差值来确定步数和增量,依次绘制直线上的点。算法实现简单,适用于任意直线,但精度较低。
|
||||
中点画线算法仅适用于斜率为0或1的直线,通过计算中点和斜率来确定直线上的像素点,效率较高。
|
||||
Bresenham算法根据直线的斜率和误差项来确定像素点的位置,适用于任意直线,且精度高,但实现稍复杂。
|
||||
date: 2026-04-11 19:01:02
|
||||
---
|
||||
|
||||
这篇文章来介绍直线扫描转换算法
|
||||
# DDA数值微分线段算法
|
||||
## 算法简介
|
||||
数值微分法即DDA法(Digital Differential Analyzer),是一种基于微分方程来生成直线的方法。在计算机图形学中,并没有线段的概念,而是一个个像素点组成了线段。
|
||||
|
||||
DDA法生成线段的步骤一般如下:
|
||||
|
||||
1. 有了起始点($x_1,y_1$)和终点($x_n,y_n$);
|
||||
2. $$\Delta x =|x_n-x_1|, \Delta y=|y_n-y_1|$$
|
||||
3. 比较$\Delta x$和$\Delta y$的大小;
|
||||
steps=$\Delta x$和$\Delta y$中较大者;
|
||||
4. $$step_x=\frac{\Delta x}{steps},step_y=\frac{\Delta y}{steps}$$
|
||||
## 算法实现
|
||||
DDA算法实现如下:
|
||||
```cpp
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <GL/glut.h>
|
||||
|
||||
// 窗口宽度和高度
|
||||
const int WIDTH = 640;
|
||||
const int HEIGHT = 480;
|
||||
|
||||
void drawDDALine(int x1, int y1, int x2, int y2) {
|
||||
float x = x1;
|
||||
float y = y1;
|
||||
|
||||
// 计算差值
|
||||
int dx = x2 - x1;
|
||||
int dy = y2 - y1;
|
||||
|
||||
// 确定步数,取 dx 和 dy 中绝对值较大的那个
|
||||
int steps = std::abs(dx) > std::abs(dy) ? std::abs(dx) : std::abs(dy); //三元表达式
|
||||
|
||||
// 计算每一步的增量
|
||||
float xIncrement = (float)dx / steps;
|
||||
float yIncrement = (float)dy / steps;
|
||||
|
||||
// 开始绘制点
|
||||
glBegin(GL_POINTS);
|
||||
glVertex2i((int)round(x), (int)round(y)); // 绘制起点
|
||||
|
||||
for (int k = 0; k < steps; k++) {
|
||||
x += xIncrement;
|
||||
y += yIncrement;
|
||||
// 将浮点坐标四舍五入取整转换为整数像素坐标
|
||||
std::cout << (int)round(x) << ", " << (int)round(y)<<"\n";
|
||||
glVertex2i((int)round(x), (int)round(y));
|
||||
}
|
||||
glEnd();
|
||||
}
|
||||
|
||||
// 显示回调函数
|
||||
void display() {
|
||||
drawDDALine(0, 0, 50, 20);
|
||||
glFlush();
|
||||
}
|
||||
|
||||
// 初始化 OpenGL 设置
|
||||
void init() {
|
||||
// 设置背景颜色为白色
|
||||
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// 设置投影矩阵为 2D 正交投影
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
// 定义可视区域,左下角(0,0),右上角(WIDTH, HEIGHT)
|
||||
gluOrtho2D(0.0, WIDTH, 0.0, HEIGHT);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// 初始化 GLUT
|
||||
glutInit(&argc, argv);
|
||||
|
||||
// 设置显示模式:单缓冲、RGB 颜色模式
|
||||
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
|
||||
|
||||
// 设置窗口大小和位置
|
||||
glutInitWindowSize(WIDTH, HEIGHT);
|
||||
glutInitWindowPosition(100, 100);
|
||||
|
||||
// 创建窗口
|
||||
glutCreateWindow("DDA算法");
|
||||
|
||||
// 注册回调函数
|
||||
glutDisplayFunc(display);
|
||||
|
||||
// 初始化设置
|
||||
init();
|
||||
|
||||
// 进入主循环
|
||||
glutMainLoop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
# 中点画线算法
|
||||
## 算法简介
|
||||
只考虑当直线的斜率$|k|< 1$时的情况,假设现在有一条直线$(x_1,y_1,x_2,y_2)$,那么第一个点一定是$(x_1,y_1)$无疑,下一个点的$x$坐标为$x_1+1$,$y$坐标要么为$y1$要么为$y1+1$。关键在于每次取下一个点时,是取前一个的$y1$呢,还是$y1+1$,这时一定是取直线上点最靠近的那个了,而判断取哪个点就用到了中点,我们将中点代入直线中 $d=F(x_1+1,y_1+0.5)=a \cdot (x_1+1)+b \cdot (y_1+0.5)+c$。
|
||||
|
||||
1. 如果直线$d>=0$,则取下边的点也就是$(x_1+1,y_1)$。
|
||||
2. 如果直线$d<0$,则取上边的点也就是$(x_1+1,y_1+1)$。
|
||||
|
||||
它的实际过程就是这样每次根据前边的点判断下一个点在哪,然后进行打亮,但这样每次判断的时候都得代入直线方程计算太麻烦了,我们将这俩种情况分别代入直线方程中可以找出规律:
|
||||
|
||||
1. 当直线$d>=0$时,经过化解得$d_1=d+a$;
|
||||
2. 当直线$d<0$时,经过化解得$d_2=d+a+b$;
|
||||
3. 初始值$d_0=a+0.5b$。
|
||||
|
||||
也就是说每次的增量要么为$a$,要么为$a+b$,那么这样判断的时候就简单多了,因为我们每次只是判断它的正负。所以给等式同时乘2,将其中浮点数0.5化为整数,这样硬件操作时无疑更快了。
|
||||
|
||||
## 算法实现
|
||||
```cpp
|
||||
#include <GL/freeglut.h>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
// 中点画线算法函数 (仅针对斜率 0 <= k <= 1 的情况进行演示,其它象限需类比处理)
|
||||
void drawLineMidpoint(int x0, int y0, int x1, int y1) {
|
||||
int a = y0 - y1;
|
||||
int b = x1 - x0;
|
||||
int d = 2 * a + b;
|
||||
int d1 = 2 * a;
|
||||
int d2 = 2 * (a + b);
|
||||
|
||||
int x = x0, y = y0;
|
||||
|
||||
glBegin(GL_POINTS);
|
||||
glVertex2i(x, y); // 画起点
|
||||
|
||||
while (x < x1) {
|
||||
if (d < 0) {
|
||||
x++;
|
||||
y++;
|
||||
d += d2;
|
||||
} else {
|
||||
x++;
|
||||
d += d1;
|
||||
}
|
||||
glVertex2i(x, y);
|
||||
}
|
||||
glEnd();
|
||||
}
|
||||
|
||||
// 渲染回调函数
|
||||
void display() {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glColor3f(1.0, 1.0, 1.0); // 设置画笔颜色为白色
|
||||
|
||||
// 调用算法画一条直线 (x0, y0) 到 (x1, y1)
|
||||
// 注意:此处的坐标对应屏幕像素坐标
|
||||
drawLineMidpoint(50, 50, 450, 300);
|
||||
|
||||
glFlush();
|
||||
}
|
||||
|
||||
// 初始化设置
|
||||
void init() {
|
||||
glClearColor(0.0, 0.0, 0.0, 1.0); // 背景设为黑色
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
// 设置正交投影矩阵,使坐标系与窗口像素对应
|
||||
gluOrtho2D(0, 500, 0, 500);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
glutInit(&argc, argv);
|
||||
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
|
||||
glutInitWindowSize(500, 500);
|
||||
glutInitWindowPosition(100, 100);
|
||||
glutCreateWindow("Midpoint Line Algorithm - FreeGLUT");
|
||||
|
||||
init();
|
||||
glutDisplayFunc(display);
|
||||
glutMainLoop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
# Bresenham算法
|
||||
## 算法简介
|
||||
假设我们需要由$(x_0, y_0)$这一点,绘画一直线至右下角的另一点$(x_1, y_1)$,x,y分别代表其水平及垂直坐标,并且$x_1 - x_0 > y_1 - y_0$。在此我们使用电脑系统常用的坐标系,即$x$坐标值沿$x$轴向右增长,$y$坐标值沿$y$轴向下增长。
|
||||
|
||||
因此x及y之值分别向右及向下增加,而两点之水平距离为$x_{1}-x_{0}$且垂直距离为$y_{1}-y_{0}$。由此得之,该线的斜率必定介乎于$0$至$1$之间。而此算法之目的,就是找出在$x_{0}$与$x_{1}$之间,第$x$行相对应的第$y$列,从而得出一像素点,使得该像素点的位置最接近原本的线。
|
||||
|
||||
对于由$(x_0, y_0)$及$(x_1, y_1)$两点所组成之直线,公式如下:
|
||||
$$y-y_{0}={\frac {y_{1}-y_{0}}{x_{1}-x_{0}}}(x-x_{0})$$
|
||||
因此,对于每一点的x,其y的值是
|
||||
$${\frac {y_{1}-y_{0}}{x_{1}-x_{0}}}(x-x_{0})+y_{0}$$
|
||||
因为$x$及$y$皆为整数,但并非每一点$x$所对应的$y$皆为整数,故此没有必要去计算每一点x所对应之$y$值。反之由于此线之斜率介乎于$1$至$0$之间,故此我们只需要找出当$x$到达那一个数值时,会使$y$上升$1$,若$x$尚未到此值,则$y$不变。至于如何找出相关的$x$值,则需依靠斜率。斜率之计算方法为$m=(y_{1}-y_{0})/(x_{1}-x_{0})$。由于此值不变,故可于运算前预先计算,减少运算次数。
|
||||
|
||||
要实行此算法,我们需计算每一像素点与该线之间的误差。于上述例子中,误差应为每一点$x$中,其相对的像素点之$y$值与该线实际之$y$值的差距。每当$y$的值增加$1$,误差的值就会增加$m$。每当误差的值超出$0.5$,线就会比较靠近下一个映像点,因此$y$的值便会加$1$,且误差减$1$。
|
||||
|
||||
## 算法实现
|
||||
```cpp
|
||||
#include <GL/freeglut.h>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
// 通用 Bresenham 画线算法
|
||||
void drawLineBresenham(int x0, int y0, int x1, int y1) {
|
||||
int dx = abs(x1 - x0);
|
||||
int dy = abs(y1 - y0);
|
||||
int sx = (x0 < x1) ? 1 : -1; // X 方向步进
|
||||
int sy = (y0 < y1) ? 1 : -1; // Y 方向步进
|
||||
int err = dx - dy; // 初始误差项
|
||||
|
||||
glBegin(GL_POINTS);
|
||||
while (true) {
|
||||
glVertex2i(x0, y0); // 绘制当前点
|
||||
|
||||
if (x0 == x1 && y0 == y1) break; // 到达终点
|
||||
|
||||
int e2 = 2 * err;
|
||||
// 判断是否在 X 方向步进
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
}
|
||||
// 判断是否在 Y 方向步进
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
glEnd();
|
||||
}
|
||||
|
||||
// 渲染回调
|
||||
void display() {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glColor3f(0.0f, 0.8f, 0.4f); // 设置一个好看的绿色(类似你图中的图标颜色)
|
||||
|
||||
// 绘制几条不同方向的线来测试算法的健壮性
|
||||
drawLineBresenham(50, 50, 450, 400); // 第一象限
|
||||
drawLineBresenham(50, 400, 450, 50); // 第四象限
|
||||
drawLineBresenham(250, 50, 250, 450); // 垂直线
|
||||
drawLineBresenham(50, 250, 450, 250); // 水平线
|
||||
|
||||
glFlush();
|
||||
}
|
||||
|
||||
void init() {
|
||||
glClearColor(0.1f, 0.1f, 0.1f, 1.0f); // 深色背景
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
gluOrtho2D(0, 500, 0, 500); // 建立 500x500 的直角坐标系
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
glutInit(&argc, argv);
|
||||
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
|
||||
glutInitWindowSize(600, 600);
|
||||
glutCreateWindow("Bresenham Line Algorithm");
|
||||
init();
|
||||
glutDisplayFunc(display);
|
||||
glutMainLoop();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: OpenGL- Cohen-Sutherland 编码裁剪算法
|
||||
cover: https://pic.biss.click/image/d1705811-324e-4378-a512-74d4baa651b7.png
|
||||
categories:
|
||||
- 技术
|
||||
- 学习
|
||||
series: OpenGL
|
||||
tags: OpenGL
|
||||
abbrlink: 9b2ef00a
|
||||
summary: >-
|
||||
这篇文章介绍了Cohen-Sutherland编码裁剪算法,这是一种在计算机图形学中常用的图像裁剪方法。裁剪的目的是从一个大场景中提取所需信息,例如在浏览地图时放大显示特定区域。Cohen-Sutherland算法通过空间划分和编码方案来判断线段与矩形窗口的位置关系,从而决定线段的哪部分在窗口内、哪部分在窗口外。算法的核心在于计算线段端点的区域编码,并根据编码值来确定线段的裁剪结果。文章还提供了具体的代码实现,包括初始化设置、窗口大小调整和主函数等,展示了如何在实际应用中使用该算法进行图像裁剪。
|
||||
date: 2026-04-15 19:34:23
|
||||
---
|
||||
|
||||
裁剪是从数据集合提取信息的过程,它是计算机图形学许多重要问题的基础。裁剪典型的用途就是从一个大的场景中提取所需的信息,以显示某一局部场景或视图。比如浏览地图时,对感兴趣的区域放大显示,此时窗口内显示的内容会相应减少。确定图形的哪些部分在窗口内,哪些部分在窗口外(不可见区域),只显示窗口内的那部分图形,这个选择处理过程就是裁剪。
|
||||
|
||||
Cohen-Sutherland编码裁剪算法
|
||||
算法思想
|
||||
1)若线段完全在窗口之内则显示该线段称为“取”,
|
||||
2)若线段明显在窗口之外则丢弃该线段称为“弃”
|
||||
3)若线段既不满足“取”的条件也不满足“弃”的条件则把线段分割为两段,对于完全在窗口之外的部分可弃之。
|
||||
|
||||
具体实现
|
||||
为快速判断一条直线段与矩形窗口的位置关系采用如图所示的空间划分和编码方案(四位二进制编码上下右左)
|
||||

|
||||
|
||||
接下来是具体代码
|
||||
|
||||
```cpp
|
||||
#include <GL/glut.h>
|
||||
#include <iostream>
|
||||
|
||||
// --- 定义区域编码 ---
|
||||
#define INSIDE 0 // 0000
|
||||
#define LEFT 1 // 0001
|
||||
#define RIGHT 2 // 0010
|
||||
#define BOTTOM 4 // 0100
|
||||
#define TOP 8 // 1000
|
||||
|
||||
// --- 定义裁剪窗口结构 ---
|
||||
struct ClipWindow {
|
||||
float xmin, xmax, ymin, ymax;
|
||||
};
|
||||
|
||||
// --- 全局变量设置 ---
|
||||
ClipWindow rect;
|
||||
int x_0, y_0, x_1, y_1; // 线段端点
|
||||
|
||||
/**
|
||||
* 计算点 (x, y) 的区域编码
|
||||
*/
|
||||
int computeCode(float x, float y, ClipWindow rect) {
|
||||
int code = INSIDE;
|
||||
if (x < rect.xmin) code |= LEFT;
|
||||
else if (x > rect.xmax) code |= RIGHT;
|
||||
if (y < rect.ymin) code |= BOTTOM;
|
||||
else if (y > rect.ymax) code |= TOP;
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cohen-Sutherland 算法核心
|
||||
* 注意:这里传入坐标的引用,直接修改坐标值以得到裁剪后的结果
|
||||
*/
|
||||
void cohenSutherlandClip(float& x_0, float& y_0, float& x_1, float& y_1, ClipWindow rect) {
|
||||
int code1 = computeCode(x_0, y_0, rect);
|
||||
int code2 = computeCode(x_1, y_1, rect);
|
||||
bool accept = false;
|
||||
|
||||
while (true) {
|
||||
// 如果两端点都在窗口内 (0000 | 0000 == 0)
|
||||
if (!(code1 | code2)) {
|
||||
accept = true;
|
||||
break;
|
||||
}
|
||||
// 如果两端点都在窗口的同一侧外部 (例如都在左边:0001 & 0001 != 0) 按位与运算
|
||||
else if (code1 & code2) {
|
||||
break; // 直接舍弃
|
||||
}
|
||||
// 3. 线段部分在窗口内,需要求交点
|
||||
else {
|
||||
int code_out;
|
||||
float x, y;
|
||||
|
||||
// 选择窗口外的一个端点
|
||||
if (code1 != 0)
|
||||
code_out = code1;
|
||||
else
|
||||
code_out = code2;
|
||||
|
||||
// 计算交点
|
||||
if (code_out & TOP) {
|
||||
x = x_0 + (x_1 - x_0) * (rect.ymax - y_0) / (y_1 - y_0);
|
||||
y = rect.ymax;
|
||||
}
|
||||
else if (code_out & BOTTOM) {
|
||||
x = x_0 + (x_1 - x_0) * (rect.ymin - y_0) / (y_1 - y_0);
|
||||
y = rect.ymin;
|
||||
}
|
||||
else if (code_out & RIGHT) {
|
||||
y = y_0 + (y_1 - y_0) * (rect.xmax - x_0) / (x_1 - x_0);
|
||||
x = rect.xmax;
|
||||
}
|
||||
else if (code_out & LEFT) {
|
||||
y = y_0 + (y_1 - y_0) * (rect.xmin - x_0) / (x_1 - x_0);
|
||||
x = rect.xmin;
|
||||
}
|
||||
|
||||
// 用交点替换原来的外部端点
|
||||
if (code_out == code1) {
|
||||
x_0 = x; y_0 = y;
|
||||
code1 = computeCode(x_0, y_0, rect);
|
||||
}
|
||||
else {
|
||||
x_1 = x; y_1 = y;
|
||||
code2 = computeCode(x_1, y_1, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果接受,则绘制裁剪后的线段(蓝色)
|
||||
if (accept) {
|
||||
glBegin(GL_LINES);
|
||||
glColor3f(0.0f, 0.0f, 1.0f); // 蓝色
|
||||
glVertex2f(x_0, y_0);
|
||||
glVertex2f(x_1, y_1);
|
||||
glEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 显示回调函数 ---
|
||||
void myDisplay() {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// 1. 绘制裁剪窗口 (红色线框)
|
||||
glColor3f(1.0f, 0.0f, 0.0f); // 红色
|
||||
glBegin(GL_LINE_LOOP);
|
||||
glVertex2f(rect.xmin, rect.ymin);
|
||||
glVertex2f(rect.xmax, rect.ymin);
|
||||
glVertex2f(rect.xmax, rect.ymax);
|
||||
glVertex2f(rect.xmin, rect.ymax);
|
||||
glEnd();
|
||||
|
||||
// 2. 绘制原始线段 (绿色)
|
||||
// 注意:为了演示效果,我们在调用裁剪函数前,先保存原始坐标,
|
||||
// 或者在这里直接用初始值绘制。这里我们使用初始值绘制。
|
||||
int orig_x_0 = 50, orig_y_0 = 350;
|
||||
int orig_x_1 = 550, orig_y_1 = 50;
|
||||
|
||||
glBegin(GL_LINES);
|
||||
glColor3f(0.0f, 1.0f, 0.0f); // 绿色
|
||||
glVertex2f(orig_x_0, orig_y_0);
|
||||
glVertex2f(orig_x_1, orig_y_1);
|
||||
glEnd();
|
||||
|
||||
// 3. 执行裁剪并绘制结果 (蓝色)
|
||||
// 使用 float 变量传入,因为算法内部会修改它们
|
||||
float cx_0 = orig_x_0, cy_0 = orig_y_0;
|
||||
float cx_1 = orig_x_1, cy_1 = orig_y_1;
|
||||
|
||||
// 加粗蓝色线条以便区分
|
||||
glLineWidth(3.0);
|
||||
cohenSutherlandClip(cx_0, cy_0, cx_1, cy_1, rect);
|
||||
glLineWidth(1.0); // 恢复线宽
|
||||
|
||||
glFlush();
|
||||
}
|
||||
|
||||
// --- 初始化设置 ---
|
||||
void Init() {
|
||||
glShadeModel(GL_FLAT);
|
||||
|
||||
rect.xmin = 100;
|
||||
rect.xmax = 400;
|
||||
rect.ymin = 100;
|
||||
rect.ymax = 300;
|
||||
}
|
||||
|
||||
// --- 窗口大小调整 ---
|
||||
void myReshape(int w, int h) {
|
||||
glViewport(0, 0, (GLsizei)w, (GLsizei)h);
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
// 设置二维投影范围,与坐标数值对应
|
||||
gluOrtho2D(0.0, (GLdouble)w, 0.0, (GLdouble)h);
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
}
|
||||
|
||||
// --- 主函数 ---
|
||||
int main(int argc, char* argv[]) {
|
||||
glutInit(&argc, argv);
|
||||
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
|
||||
glutInitWindowPosition(100, 100);
|
||||
glutInitWindowSize(600, 400);
|
||||
glutCreateWindow("实验2_03毕爽爽 Cohen-Sutherland 算法");
|
||||
|
||||
Init();
|
||||
|
||||
glutDisplayFunc(myDisplay);
|
||||
glutReshapeFunc(myReshape);
|
||||
|
||||
glutMainLoop();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: 把普通的一天过成自己的节奏
|
||||
categories:
|
||||
- 生活
|
||||
tags:
|
||||
- 随笔
|
||||
- 生活
|
||||
abbrlink: efe1e805
|
||||
date: 2026-05-29 18:09:40
|
||||
---
|
||||
|
||||
有时候,我们会把“好好生活”想得太大,好像它必须绑定某个明确的目标:换一份更好的工作,去一个更远的地方,学会一项很厉害的技能,或者在某个时间节点之前完成别人眼里的成功。
|
||||
|
||||
可真正能把日子托住的,往往不是这些很大的东西,而是一些小到容易被忽略的瞬间。
|
||||
|
||||
比如早上醒来后,没有立刻被消息推着跑,而是先喝一口水,慢慢把自己放回这一天里。比如走在路上时,发现今天的风刚好不冷不热,树影落在地上,像一张没人署名的明信片。比如晚上回到房间,给桌面留出一点空白,也给心里留出一点空白。
|
||||
|
||||
生活当然不会因为这些细节就变得没有压力。该做的事情还在那里,该面对的问题也不会自动消失。但这些小事会提醒我们:我不是只在处理任务,我也正在经历自己的人生。
|
||||
|
||||
很多时候,人会被“必须更快一点”的感觉追着走。快一点回复,快一点完成,快一点变好。久而久之,我们甚至忘了问自己:我到底想把时间用成什么样子?
|
||||
|
||||
也许答案并不复杂。
|
||||
|
||||
可以是认真吃一顿饭,不一边刷手机一边把味道错过。可以是把拖了很久的小事做完,让它别再占用心里的后台。可以是读几页书,听一首歌,散一会儿步,或者只是安静地发一会儿呆。
|
||||
|
||||
这些事情看起来不够“高效”,却很像给生活校准方向。它们让人重新感觉到,日子不是一条只能向前赶的进度条,也可以是一段可以被感受、被整理、被轻轻收藏的时间。
|
||||
|
||||
普通的一天并不会自动变得特别。但如果我们愿意在里面放进一点自己的节奏,它就会慢慢变得有辨识度。
|
||||
|
||||
不是为了把每一天都过得闪闪发光,而是为了在平凡里仍然知道:这是我的一天,我在里面认真地活过。
|
||||
Reference in New Issue
Block a user