ezal-theme-example 0.0.1
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/LICENSE +21 -0
- package/assets/scripts/404.ts +353 -0
- package/assets/scripts/_article.ts +290 -0
- package/assets/scripts/_base.ts +65 -0
- package/assets/scripts/_pagefind.d.ts +424 -0
- package/assets/scripts/_search.ts +88 -0
- package/assets/scripts/_utils.ts +74 -0
- package/assets/scripts/archive.ts +143 -0
- package/assets/scripts/article.ts +18 -0
- package/assets/scripts/category.ts +4 -0
- package/assets/scripts/home.ts +73 -0
- package/assets/scripts/links.ts +14 -0
- package/assets/scripts/main.ts +11 -0
- package/assets/scripts/page.ts +11 -0
- package/assets/scripts/tag.ts +4 -0
- package/assets/scripts/tsconfig.json +10 -0
- package/assets/styles/404.styl +31 -0
- package/assets/styles/_article/fold.styl +15 -0
- package/assets/styles/_article/footnote.styl +12 -0
- package/assets/styles/_article/heading.styl +29 -0
- package/assets/styles/_article/image.styl +30 -0
- package/assets/styles/_article/kbd.styl +10 -0
- package/assets/styles/_article/links.styl +31 -0
- package/assets/styles/_article/list.styl +19 -0
- package/assets/styles/_article/note.styl +18 -0
- package/assets/styles/_article/other.styl +44 -0
- package/assets/styles/_article/table.styl +29 -0
- package/assets/styles/_article/tabs.styl +25 -0
- package/assets/styles/_code.styl +83 -0
- package/assets/styles/_index/contact.styl +20 -0
- package/assets/styles/_index/footer.styl +5 -0
- package/assets/styles/_index/header.styl +40 -0
- package/assets/styles/_index/nav.styl +59 -0
- package/assets/styles/_index/search.styl +64 -0
- package/assets/styles/_index.styl +91 -0
- package/assets/styles/_var.styl +96 -0
- package/assets/styles/archive.styl +35 -0
- package/assets/styles/article.styl +138 -0
- package/assets/styles/category.styl +4 -0
- package/assets/styles/home.styl +124 -0
- package/assets/styles/links.styl +121 -0
- package/assets/styles/page.styl +12 -0
- package/assets/styles/tag.styl +4 -0
- package/dist/config.d.ts +128 -0
- package/dist/feed.d.ts +4 -0
- package/dist/image/asset.d.ts +22 -0
- package/dist/image/db.d.ts +18 -0
- package/dist/image/index.d.ts +10 -0
- package/dist/image/metadata.d.ts +2 -0
- package/dist/image/utils.d.ts +1 -0
- package/dist/index-now.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2066 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/markdown/codeblock/data.d.ts +1 -0
- package/dist/markdown/codeblock/index.d.ts +6 -0
- package/dist/markdown/codeblock/style.d.ts +2 -0
- package/dist/markdown/fold.d.ts +6 -0
- package/dist/markdown/footnote.d.ts +15 -0
- package/dist/markdown/image.d.ts +12 -0
- package/dist/markdown/index.d.ts +2 -0
- package/dist/markdown/kbd.d.ts +6 -0
- package/dist/markdown/link.d.ts +2 -0
- package/dist/markdown/links.d.ts +12 -0
- package/dist/markdown/note.d.ts +8 -0
- package/dist/markdown/table.d.ts +3 -0
- package/dist/markdown/tabs.d.ts +7 -0
- package/dist/markdown/tex.d.ts +9 -0
- package/dist/page/404.d.ts +1 -0
- package/dist/page/archive.d.ts +1 -0
- package/dist/page/category.d.ts +1 -0
- package/dist/page/home.d.ts +2 -0
- package/dist/page/tag.d.ts +1 -0
- package/dist/pagefind.d.ts +20 -0
- package/dist/sitemap.d.ts +2 -0
- package/dist/transform/script.d.ts +2 -0
- package/dist/transform/stylus.d.ts +2 -0
- package/dist/utils.d.ts +2 -0
- package/layouts/404.tsx +8 -0
- package/layouts/archive.tsx +81 -0
- package/layouts/article.tsx +145 -0
- package/layouts/base.tsx +20 -0
- package/layouts/category.tsx +18 -0
- package/layouts/components/ArchiveArticleList.tsx +14 -0
- package/layouts/components/Article.tsx +46 -0
- package/layouts/components/Contact.tsx +14 -0
- package/layouts/components/Footer.tsx +44 -0
- package/layouts/components/Head.tsx +119 -0
- package/layouts/components/Image.tsx +42 -0
- package/layouts/components/Nav.tsx +33 -0
- package/layouts/components/Search.tsx +20 -0
- package/layouts/components/Waline.tsx +22 -0
- package/layouts/context.d.ts +54 -0
- package/layouts/home.tsx +74 -0
- package/layouts/links.tsx +53 -0
- package/layouts/page.tsx +19 -0
- package/layouts/tag.tsx +18 -0
- package/layouts/tsconfig.json +11 -0
- package/package.json +47 -0
- package/readme.md +17 -0
- package/readme_zh.md +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jonny
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { initBase } from './_base';
|
|
2
|
+
import { $, doc, handle } from './_utils';
|
|
3
|
+
|
|
4
|
+
class GlitchTextEffect {
|
|
5
|
+
canvas: HTMLCanvasElement;
|
|
6
|
+
text: string;
|
|
7
|
+
fontSize: number;
|
|
8
|
+
gl: WebGLRenderingContext | null = null;
|
|
9
|
+
program: WebGLProgram | null = null;
|
|
10
|
+
startTime: number;
|
|
11
|
+
timeLocation: WebGLUniformLocation | null = null;
|
|
12
|
+
resolutionLocation: WebGLUniformLocation | null = null;
|
|
13
|
+
textLocation: WebGLUniformLocation | null = null;
|
|
14
|
+
textTexture: WebGLTexture | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(canvas: HTMLCanvasElement, text: string, fontSize: number) {
|
|
17
|
+
this.canvas = canvas;
|
|
18
|
+
|
|
19
|
+
this.text = text;
|
|
20
|
+
this.fontSize = fontSize;
|
|
21
|
+
this.startTime = Date.now();
|
|
22
|
+
|
|
23
|
+
this.initWebGL();
|
|
24
|
+
this.createShaders();
|
|
25
|
+
this.setupBuffers();
|
|
26
|
+
this.animate();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initWebGL(): void {
|
|
30
|
+
const gl =
|
|
31
|
+
this.canvas.getContext('webgl', { alpha: true, antialias: true }) ??
|
|
32
|
+
this.canvas.getContext('experimental-webgl', {
|
|
33
|
+
alpha: true,
|
|
34
|
+
antialias: true,
|
|
35
|
+
});
|
|
36
|
+
if (!gl) {
|
|
37
|
+
throw new Error('WebGL not supported');
|
|
38
|
+
}
|
|
39
|
+
this.gl = gl as any;
|
|
40
|
+
this.gl!.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createShaders(): void {
|
|
44
|
+
if (!this.gl) return;
|
|
45
|
+
|
|
46
|
+
const vsSource = `
|
|
47
|
+
attribute vec2 aPosition;
|
|
48
|
+
varying vec2 vUv;
|
|
49
|
+
|
|
50
|
+
void main() {
|
|
51
|
+
vUv = aPosition * 0.5 + 0.5;
|
|
52
|
+
gl_Position = vec4(aPosition, 0.0, 1.0);
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const fsSource = `
|
|
57
|
+
precision mediump float;
|
|
58
|
+
|
|
59
|
+
varying vec2 vUv;
|
|
60
|
+
uniform float uTime;
|
|
61
|
+
uniform vec2 uResolution;
|
|
62
|
+
uniform sampler2D uTexture;
|
|
63
|
+
|
|
64
|
+
// 伪随机数生成器
|
|
65
|
+
float random (in vec2 st) {
|
|
66
|
+
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void main() {
|
|
70
|
+
vec2 st = gl_FragCoord.xy / uResolution.xy;
|
|
71
|
+
|
|
72
|
+
// 基础UV坐标
|
|
73
|
+
vec2 uv = vUv;
|
|
74
|
+
|
|
75
|
+
// 计算文字纹理坐标
|
|
76
|
+
vec2 textUV = vec2(uv.x, 1.0 - uv.y); // 翻转Y轴以匹配纹理
|
|
77
|
+
|
|
78
|
+
// 信号错位故障效果
|
|
79
|
+
float timeFactor = uTime * 1.5;
|
|
80
|
+
float glitchIntensity = abs(sin(timeFactor * 0.7)) * 0.8;
|
|
81
|
+
|
|
82
|
+
// 检查是否触发故障
|
|
83
|
+
float glitchEvent = random(vec2(floor(uTime * 4.0)));
|
|
84
|
+
|
|
85
|
+
// 初始化偏移
|
|
86
|
+
vec2 offset = vec2(0.0);
|
|
87
|
+
|
|
88
|
+
// 水平撕裂/错位效果
|
|
89
|
+
if (glitchEvent > 0.5) {
|
|
90
|
+
// 生成多个水平撕裂线
|
|
91
|
+
float tearCount = 3.0 + mod(uTime * 0.5, 3.0); // 撕裂线条数
|
|
92
|
+
|
|
93
|
+
for (float i = 0.0; i < 5.0; i++) {
|
|
94
|
+
if (i >= tearCount) break;
|
|
95
|
+
|
|
96
|
+
float tearY = random(vec2(i, floor(uTime * 2.0 + i))) * 1.0; // 随机水平线位置
|
|
97
|
+
float tearHeight = 0.02 + random(vec2(i * 2.0)) * 0.08; // 撕裂区域高度
|
|
98
|
+
float shiftAmount = (random(vec2(i * 3.0)) - 0.5) * 0.1 * glitchIntensity; // 偏移量
|
|
99
|
+
|
|
100
|
+
if (abs(textUV.y - tearY) < tearHeight) {
|
|
101
|
+
offset.x += shiftAmount;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 垂直撕裂效果
|
|
107
|
+
float verticalGlitch = random(vec2(floor(uTime * 6.0), 2.0));
|
|
108
|
+
if (verticalGlitch > 0.8) {
|
|
109
|
+
float tearX = random(vec2(2.0, floor(uTime * 3.0))) * 1.0; // 随机垂直线位置
|
|
110
|
+
float tearWidth = 0.02 + random(vec2(4.0)) * 0.05; // 撕裂区域宽度
|
|
111
|
+
float shiftAmount = (random(vec2(5.0)) - 0.5) * 0.1 * glitchIntensity; // 垂直偏移量
|
|
112
|
+
|
|
113
|
+
if (abs(textUV.x - tearX) < tearWidth) {
|
|
114
|
+
offset.y += shiftAmount;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 块状故障效果
|
|
119
|
+
float blockGlitch = random(vec2(floor(uTime * 2.0), 3.0));
|
|
120
|
+
if (blockGlitch > 0.9) {
|
|
121
|
+
vec2 blockSize = vec2(0.1, 0.05) + vec2(random(vec2(6.0)), random(vec2(7.0))) * 0.1;
|
|
122
|
+
vec2 blockPos = vec2(random(vec2(8.0, floor(uTime))), random(vec2(9.0, floor(uTime * 1.5))));
|
|
123
|
+
|
|
124
|
+
if (textUV.x > blockPos.x && textUV.x < blockPos.x + blockSize.x &&
|
|
125
|
+
textUV.y > blockPos.y && textUV.y < blockPos.y + blockSize.y) {
|
|
126
|
+
// 块状区域偏移
|
|
127
|
+
offset += vec2(
|
|
128
|
+
(random(vec2(10.0)) - 0.5) * 0.1 * glitchIntensity,
|
|
129
|
+
(random(vec2(11.0)) - 0.5) * 0.05 * glitchIntensity
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 颜色通道分离
|
|
135
|
+
vec2 offsetR = offset + vec2(
|
|
136
|
+
sin(uTime * 8.0 + textUV.y * 5.0) * 0.005 * glitchIntensity,
|
|
137
|
+
0.0
|
|
138
|
+
);
|
|
139
|
+
vec2 offsetG = offset + vec2(
|
|
140
|
+
cos(uTime * 7.0 + textUV.x * 4.0) * 0.003 * glitchIntensity,
|
|
141
|
+
0.0
|
|
142
|
+
);
|
|
143
|
+
vec2 offsetB = offset + vec2(
|
|
144
|
+
sin(uTime * 9.0 + (textUV.x + textUV.y) * 3.0) * 0.007 * glitchIntensity,
|
|
145
|
+
0.0
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// 颜色通道分离
|
|
149
|
+
float r = texture2D(uTexture, textUV + offsetR).r;
|
|
150
|
+
float g = texture2D(uTexture, textUV + offsetG).g;
|
|
151
|
+
float b = texture2D(uTexture, textUV + offsetB).b;
|
|
152
|
+
|
|
153
|
+
// 主文字颜色
|
|
154
|
+
vec4 baseColor = texture2D(uTexture, textUV + offset);
|
|
155
|
+
|
|
156
|
+
// 创建故障颜色
|
|
157
|
+
vec3 glitchColor = vec3(r, g, b);
|
|
158
|
+
|
|
159
|
+
// 混合原始颜色和故障颜色
|
|
160
|
+
vec3 finalColor = mix(baseColor.rgb, glitchColor, 0.4);
|
|
161
|
+
|
|
162
|
+
// 偶尔的色差效果
|
|
163
|
+
if (random(vec2(floor(uTime * 5.0))) > 0.92) {
|
|
164
|
+
finalColor = vec3(r, b, g); // 随机交换颜色通道
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 透明度处理
|
|
168
|
+
gl_FragColor = vec4(finalColor, baseColor.a);
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
|
|
173
|
+
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
|
|
174
|
+
|
|
175
|
+
if (!(vertexShader && fragmentShader)) {
|
|
176
|
+
throw new Error('Failed to load shaders');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.program = this.gl.createProgram();
|
|
180
|
+
if (!this.program) {
|
|
181
|
+
throw new Error('Failed to create WebGL program');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.gl.attachShader(this.program, vertexShader);
|
|
185
|
+
this.gl.attachShader(this.program, fragmentShader);
|
|
186
|
+
this.gl.linkProgram(this.program);
|
|
187
|
+
|
|
188
|
+
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
'Unable to initialize the shader program: ' +
|
|
191
|
+
this.gl.getProgramInfoLog(this.program),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.gl.useProgram(this.program);
|
|
196
|
+
|
|
197
|
+
// 获取 uniform 变量位置
|
|
198
|
+
this.timeLocation = this.gl.getUniformLocation(this.program, 'uTime');
|
|
199
|
+
this.resolutionLocation = this.gl.getUniformLocation(
|
|
200
|
+
this.program,
|
|
201
|
+
'uResolution',
|
|
202
|
+
);
|
|
203
|
+
this.textLocation = this.gl.getUniformLocation(this.program, 'uTexture');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
loadShader(type: number, source: string): WebGLShader | null {
|
|
207
|
+
if (!this.gl) return null;
|
|
208
|
+
|
|
209
|
+
const shader = this.gl.createShader(type);
|
|
210
|
+
if (!shader) {
|
|
211
|
+
console.error('Unable to create shader');
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.gl.shaderSource(shader, source);
|
|
216
|
+
this.gl.compileShader(shader);
|
|
217
|
+
|
|
218
|
+
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
219
|
+
console.error(
|
|
220
|
+
'An error occurred compiling the shaders: ' +
|
|
221
|
+
this.gl.getShaderInfoLog(shader),
|
|
222
|
+
);
|
|
223
|
+
this.gl.deleteShader(shader);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return shader;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
setupBuffers(): void {
|
|
231
|
+
if (!this.gl) return;
|
|
232
|
+
|
|
233
|
+
// 创建一个覆盖整个视口的四边形
|
|
234
|
+
const vertices = new Float32Array([
|
|
235
|
+
-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
const vertexBuffer = this.gl.createBuffer();
|
|
239
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer);
|
|
240
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
|
|
241
|
+
|
|
242
|
+
const positionLocation = this.gl.getAttribLocation(
|
|
243
|
+
this.program!,
|
|
244
|
+
'aPosition',
|
|
245
|
+
);
|
|
246
|
+
if (positionLocation !== -1) {
|
|
247
|
+
this.gl.enableVertexAttribArray(positionLocation);
|
|
248
|
+
this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 0, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 创建纹理并绘制文字
|
|
252
|
+
this.createTextTexture();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
createTextTexture(): void {
|
|
256
|
+
if (!this.gl) return;
|
|
257
|
+
|
|
258
|
+
// 创建一个临时canvas来绘制文字
|
|
259
|
+
const tempCanvas = document.createElement('canvas');
|
|
260
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
261
|
+
|
|
262
|
+
if (!tempCtx) {
|
|
263
|
+
throw new Error('Could not get 2D context for text canvas');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 设置canvas大小 - 使用更大的尺寸以获得更高分辨率的文字纹理
|
|
267
|
+
tempCanvas.width = 1600;
|
|
268
|
+
tempCanvas.height = 600;
|
|
269
|
+
|
|
270
|
+
// 绘制文字
|
|
271
|
+
tempCtx.fillStyle = 'white';
|
|
272
|
+
tempCtx.font = `bold ${this.fontSize}px sans-serif`;
|
|
273
|
+
tempCtx.textAlign = 'center';
|
|
274
|
+
tempCtx.textBaseline = 'middle';
|
|
275
|
+
tempCtx.fillText(this.text, tempCanvas.width / 2, tempCanvas.height / 2);
|
|
276
|
+
|
|
277
|
+
// 创建纹理
|
|
278
|
+
this.textTexture = this.gl.createTexture();
|
|
279
|
+
if (!this.textTexture) {
|
|
280
|
+
throw new Error('Failed to create WebGL texture');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.textTexture);
|
|
284
|
+
|
|
285
|
+
// 设置纹理参数
|
|
286
|
+
this.gl.texParameteri(
|
|
287
|
+
this.gl.TEXTURE_2D,
|
|
288
|
+
this.gl.TEXTURE_WRAP_S,
|
|
289
|
+
this.gl.CLAMP_TO_EDGE,
|
|
290
|
+
);
|
|
291
|
+
this.gl.texParameteri(
|
|
292
|
+
this.gl.TEXTURE_2D,
|
|
293
|
+
this.gl.TEXTURE_WRAP_T,
|
|
294
|
+
this.gl.CLAMP_TO_EDGE,
|
|
295
|
+
);
|
|
296
|
+
this.gl.texParameteri(
|
|
297
|
+
this.gl.TEXTURE_2D,
|
|
298
|
+
this.gl.TEXTURE_MIN_FILTER,
|
|
299
|
+
this.gl.LINEAR,
|
|
300
|
+
);
|
|
301
|
+
this.gl.texParameteri(
|
|
302
|
+
this.gl.TEXTURE_2D,
|
|
303
|
+
this.gl.TEXTURE_MAG_FILTER,
|
|
304
|
+
this.gl.LINEAR,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// 将canvas内容上传到纹理
|
|
308
|
+
this.gl.texImage2D(
|
|
309
|
+
this.gl.TEXTURE_2D,
|
|
310
|
+
0,
|
|
311
|
+
this.gl.RGBA,
|
|
312
|
+
this.gl.RGBA,
|
|
313
|
+
this.gl.UNSIGNED_BYTE,
|
|
314
|
+
tempCanvas,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
animate = (): void => {
|
|
319
|
+
if (!this.gl) return;
|
|
320
|
+
requestAnimationFrame(this.animate);
|
|
321
|
+
// 清除画布 - 使用透明色
|
|
322
|
+
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
323
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
324
|
+
|
|
325
|
+
// 激活纹理单元并绑定纹理
|
|
326
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
327
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.textTexture as WebGLTexture);
|
|
328
|
+
if (this.textLocation) {
|
|
329
|
+
this.gl.uniform1i(this.textLocation, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 设置 uniform 变量
|
|
333
|
+
const currentTime = (Date.now() - this.startTime) / 1000;
|
|
334
|
+
if (this.timeLocation) {
|
|
335
|
+
this.gl.uniform1f(this.timeLocation, currentTime);
|
|
336
|
+
}
|
|
337
|
+
if (this.resolutionLocation) {
|
|
338
|
+
this.gl.uniform2f(
|
|
339
|
+
this.resolutionLocation,
|
|
340
|
+
this.canvas.width,
|
|
341
|
+
this.canvas.height,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 绘制
|
|
346
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
handle(doc, 'DOMContentLoaded', () => {
|
|
351
|
+
initBase();
|
|
352
|
+
new GlitchTextEffect($('canvas')!, '404', 600);
|
|
353
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import Hammer from 'hammerjs';
|
|
2
|
+
import { $, $$, $new, doc, handle, offHandle, sleep } from './_utils';
|
|
3
|
+
|
|
4
|
+
export function initTabs() {
|
|
5
|
+
handle(doc, 'change', (event) => {
|
|
6
|
+
let target: Element | null = event.target as Element;
|
|
7
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
8
|
+
if (target.type !== 'radio') return;
|
|
9
|
+
if (!target.id.startsWith('tab')) return;
|
|
10
|
+
if (!target.name.startsWith('tab')) return;
|
|
11
|
+
target = target.parentElement;
|
|
12
|
+
if (!target) return;
|
|
13
|
+
const content = target.parentElement?.nextElementSibling;
|
|
14
|
+
if (!(content instanceof HTMLElement)) return;
|
|
15
|
+
for (const element of $$(':scope>.active', content)) {
|
|
16
|
+
element.classList.remove('active');
|
|
17
|
+
}
|
|
18
|
+
const index = [...target.parentElement!.children].indexOf(target);
|
|
19
|
+
content.children.item(index)?.classList.add('active');
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function initToc() {
|
|
24
|
+
const toc = [...$$('aside a')];
|
|
25
|
+
if (toc.length === 0) return;
|
|
26
|
+
const headings = [...$$('h2,h3,h4,h5,h6', $('article')!)];
|
|
27
|
+
const docE = doc.documentElement;
|
|
28
|
+
|
|
29
|
+
function getIndex(): number {
|
|
30
|
+
if (docE.scrollTop < 100) return 0;
|
|
31
|
+
if (docE.scrollTop + innerHeight >= docE.scrollHeight - 5) {
|
|
32
|
+
return toc.length - 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const triggerLine = innerHeight / 3;
|
|
36
|
+
for (const [i, heading] of headings.entries()) {
|
|
37
|
+
const { top } = heading.getBoundingClientRect();
|
|
38
|
+
if (top > triggerLine) return Math.max(0, i - 1);
|
|
39
|
+
}
|
|
40
|
+
return toc.length - 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handle(doc, 'scroll', () => {
|
|
44
|
+
const index = getIndex();
|
|
45
|
+
const target = toc[index];
|
|
46
|
+
|
|
47
|
+
if (target.classList.contains('active')) return;
|
|
48
|
+
for (const element of $$('aside .active')) {
|
|
49
|
+
element.classList.remove('active');
|
|
50
|
+
}
|
|
51
|
+
target.classList.add('active');
|
|
52
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const nav = $<HTMLElement>('nav')!;
|
|
56
|
+
const mobileToc = $('.toc')!.cloneNode(true) as HTMLElement;
|
|
57
|
+
nav.append(mobileToc);
|
|
58
|
+
const height = () => {
|
|
59
|
+
nav.style.maxHeight = `${nav.scrollHeight}px`;
|
|
60
|
+
};
|
|
61
|
+
handle(window, 'resize', height);
|
|
62
|
+
for (const a of $$('a', mobileToc)) {
|
|
63
|
+
handle(a, 'click', async () => {
|
|
64
|
+
nav.classList.remove('nav-show');
|
|
65
|
+
await sleep(10);
|
|
66
|
+
nav.classList.add('nav-hide');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
height();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function initCodeblock(scope: ParentNode = doc) {
|
|
73
|
+
for (const btn of $$<HTMLButtonElement>('.code button', scope)) {
|
|
74
|
+
handle(btn, 'click', async () => {
|
|
75
|
+
await navigator.clipboard.writeText(
|
|
76
|
+
btn.parentNode!.nextSibling!.textContent!,
|
|
77
|
+
);
|
|
78
|
+
btn.classList.replace('icon-copy', 'icon-check');
|
|
79
|
+
await sleep(1000);
|
|
80
|
+
btn.classList.replace('icon-check', 'icon-copy');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function initFootnote() {
|
|
86
|
+
let hoveringRef = false;
|
|
87
|
+
let hoveringPreview = false;
|
|
88
|
+
const preview = $new('div');
|
|
89
|
+
preview.className = 'preview rounded';
|
|
90
|
+
async function refIn(this: HTMLAnchorElement) {
|
|
91
|
+
const [_, id] = this.href.split('#');
|
|
92
|
+
if (!id) return;
|
|
93
|
+
const dt = doc.getElementById(id);
|
|
94
|
+
if (!dt) return;
|
|
95
|
+
const dd = dt.nextSibling as HTMLElement;
|
|
96
|
+
if (dd?.tagName !== 'DD') return;
|
|
97
|
+
preview.replaceChildren(dd.cloneNode(true));
|
|
98
|
+
initCodeblock(preview);
|
|
99
|
+
for (const label of $$<HTMLLabelElement>('.tab-nav label', preview)) {
|
|
100
|
+
label.htmlFor += '-copy';
|
|
101
|
+
}
|
|
102
|
+
for (const radio of $$<HTMLInputElement>('.tab-nav input', preview)) {
|
|
103
|
+
radio.id += '-copy';
|
|
104
|
+
radio.name += '-copy';
|
|
105
|
+
}
|
|
106
|
+
doc.body.append(preview);
|
|
107
|
+
hoveringRef = true;
|
|
108
|
+
|
|
109
|
+
await sleep();
|
|
110
|
+
|
|
111
|
+
const { width, height } = preview.getBoundingClientRect();
|
|
112
|
+
const { top, right, bottom, left } = this.getBoundingClientRect();
|
|
113
|
+
if (left + width <= innerWidth) {
|
|
114
|
+
preview.style.left = `${left}px`;
|
|
115
|
+
} else if (right - width >= 0) {
|
|
116
|
+
preview.style.left = `${right - width}px`;
|
|
117
|
+
} else {
|
|
118
|
+
preview.style.left = `${innerWidth - width}px`;
|
|
119
|
+
}
|
|
120
|
+
if (bottom + height + 8 <= innerHeight) {
|
|
121
|
+
preview.style.top = `${bottom + 8}px`;
|
|
122
|
+
} else if (top - height >= 0) {
|
|
123
|
+
preview.style.top = `${top - height}px`;
|
|
124
|
+
} else {
|
|
125
|
+
preview.style.top = '0px';
|
|
126
|
+
}
|
|
127
|
+
preview.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 100 });
|
|
128
|
+
}
|
|
129
|
+
async function refOut(this: HTMLAnchorElement) {
|
|
130
|
+
hoveringRef = false;
|
|
131
|
+
await sleep(100);
|
|
132
|
+
if (hoveringPreview || hoveringRef) return;
|
|
133
|
+
preview.animate([{ opacity: 0 }], { duration: 100 }).onfinish = () =>
|
|
134
|
+
preview.remove();
|
|
135
|
+
}
|
|
136
|
+
for (const ref of $$<HTMLAnchorElement>('article .footnote')) {
|
|
137
|
+
handle(ref, 'mouseenter', refIn);
|
|
138
|
+
handle(ref, 'mouseout', refOut);
|
|
139
|
+
handle(ref, 'focusin', refIn);
|
|
140
|
+
handle(ref, 'focusout', refOut);
|
|
141
|
+
}
|
|
142
|
+
handle(preview, 'mouseenter', () => {
|
|
143
|
+
hoveringPreview = true;
|
|
144
|
+
});
|
|
145
|
+
handle(preview, 'mouseleave', async () => {
|
|
146
|
+
hoveringPreview = false;
|
|
147
|
+
await sleep(100);
|
|
148
|
+
if (hoveringPreview || hoveringRef) return;
|
|
149
|
+
preview.animate([{ opacity: 0 }], { duration: 100 }).onfinish = () =>
|
|
150
|
+
preview.remove();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function display(image: HTMLImageElement) {
|
|
155
|
+
const { top, left, width } = image.getBoundingClientRect();
|
|
156
|
+
const boxFrame = { background: '#0000' };
|
|
157
|
+
const frame = { top: `${top}px`, left: `${left}px`, width: `${width}px` };
|
|
158
|
+
const defaultDuration: number = 100;
|
|
159
|
+
const naturalWidth = image.naturalWidth;
|
|
160
|
+
const naturalHeight = image.naturalHeight;
|
|
161
|
+
let currentWidth = 0;
|
|
162
|
+
let currentHeight = 0;
|
|
163
|
+
const box = $new('div');
|
|
164
|
+
box.classList.add('img-scale');
|
|
165
|
+
const img = $new('img');
|
|
166
|
+
box.append(img);
|
|
167
|
+
img.src = image.currentSrc;
|
|
168
|
+
img.width = naturalWidth;
|
|
169
|
+
img.height = naturalHeight;
|
|
170
|
+
img.draggable = false;
|
|
171
|
+
|
|
172
|
+
let currentX = 0;
|
|
173
|
+
let currentY = 0;
|
|
174
|
+
let currentScale = 1;
|
|
175
|
+
|
|
176
|
+
const hammer = new Hammer(box, {
|
|
177
|
+
recognizers: [
|
|
178
|
+
[Hammer.Tap, { time: 250, threshold: 10 }],
|
|
179
|
+
[Hammer.Pan, { direction: Hammer.DIRECTION_ALL, threshold: 1 }],
|
|
180
|
+
[Hammer.Pinch, { enable: true }],
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
hammer.get('pinch').recognizeWith('pan');
|
|
184
|
+
hammer.get('pan').recognizeWith('pinch');
|
|
185
|
+
|
|
186
|
+
const hide = () => {
|
|
187
|
+
img.animate({ ...frame, scale: 1, translate: '0px 0px' }, defaultDuration);
|
|
188
|
+
const animation = box.animate(boxFrame, defaultDuration);
|
|
189
|
+
animation.onfinish = () => {
|
|
190
|
+
box.remove();
|
|
191
|
+
image.style.opacity = '';
|
|
192
|
+
};
|
|
193
|
+
animation.play();
|
|
194
|
+
hammer.destroy();
|
|
195
|
+
offHandle(window, 'resize', resize);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const fitScale = () => {
|
|
199
|
+
const min =
|
|
200
|
+
Math.min(innerWidth / currentWidth, innerHeight / currentHeight, 1) / 2;
|
|
201
|
+
const max =
|
|
202
|
+
Math.max(currentWidth / innerWidth, currentHeight / innerHeight) * 8;
|
|
203
|
+
currentScale = Math.min(Math.max(currentScale, min), max);
|
|
204
|
+
img.style.scale = `${currentScale}`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const fitPosition = () => {
|
|
208
|
+
const displayWidth = currentWidth * currentScale;
|
|
209
|
+
const displayHeight = currentHeight * currentScale;
|
|
210
|
+
const limitX = Math.max((displayWidth - innerWidth) / 2, 0);
|
|
211
|
+
currentX = Math.min(Math.max(currentX, -limitX), limitX);
|
|
212
|
+
const limitY = Math.max((displayHeight - innerHeight) / 2, 0);
|
|
213
|
+
currentY = Math.min(Math.max(currentY, -limitY), limitY);
|
|
214
|
+
img.style.translate = `${currentX}px ${currentY}px`;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const resize = () => {
|
|
218
|
+
currentWidth = Math.min(
|
|
219
|
+
naturalWidth,
|
|
220
|
+
innerWidth,
|
|
221
|
+
(innerHeight / naturalHeight) * naturalWidth,
|
|
222
|
+
);
|
|
223
|
+
currentHeight = (naturalHeight / naturalWidth) * currentWidth;
|
|
224
|
+
img.style.top = `${(innerHeight - currentHeight) / 2}px`;
|
|
225
|
+
img.style.left = `${(innerWidth - currentWidth) / 2}px`;
|
|
226
|
+
img.style.width = `${currentWidth}px`;
|
|
227
|
+
fitPosition();
|
|
228
|
+
fitScale();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
resize();
|
|
232
|
+
doc.body.append(box);
|
|
233
|
+
box.animate([boxFrame, {}], defaultDuration).play();
|
|
234
|
+
img.animate([frame, {}], { duration: 300, easing: 'ease-in-out' }).play();
|
|
235
|
+
image.style.opacity = '0';
|
|
236
|
+
|
|
237
|
+
let dragging = false;
|
|
238
|
+
|
|
239
|
+
// 点击
|
|
240
|
+
let tapTimer: number | null = null;
|
|
241
|
+
hammer.on('tap', ({ pointerType }) => {
|
|
242
|
+
if (pointerType !== 'touch') return hide();
|
|
243
|
+
if (tapTimer === null) {
|
|
244
|
+
tapTimer = setTimeout(hide, 200);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
clearTimeout(tapTimer);
|
|
248
|
+
tapTimer = null;
|
|
249
|
+
const prev = currentScale;
|
|
250
|
+
if (currentScale < 4) currentScale *= 2;
|
|
251
|
+
else currentScale = 1;
|
|
252
|
+
img.style.scale = `${currentScale}`;
|
|
253
|
+
img.animate([{ scale: prev }, {}], defaultDuration);
|
|
254
|
+
});
|
|
255
|
+
// 滚轮缩放
|
|
256
|
+
handle(box, 'wheel', (event: WheelEvent) => {
|
|
257
|
+
if (event.deltaY < 0) currentScale *= 1.5;
|
|
258
|
+
else currentScale /= 1.5;
|
|
259
|
+
fitScale();
|
|
260
|
+
if (!dragging) fitPosition();
|
|
261
|
+
});
|
|
262
|
+
// 缩放 & 移动
|
|
263
|
+
hammer.on('pan pinch', ({ deltaX, deltaY, scale }) => {
|
|
264
|
+
dragging = true;
|
|
265
|
+
img.style.translate = `${currentX + deltaX}px ${currentY + deltaY}px`;
|
|
266
|
+
img.style.scale = `${currentScale * scale}`;
|
|
267
|
+
});
|
|
268
|
+
hammer.on('panend', ({ deltaX, deltaY }) => {
|
|
269
|
+
currentX += deltaX;
|
|
270
|
+
currentY += deltaY;
|
|
271
|
+
dragging = false;
|
|
272
|
+
const prevX = currentX;
|
|
273
|
+
const prevY = currentY;
|
|
274
|
+
fitPosition();
|
|
275
|
+
img.animate([{ translate: `${prevX}px ${prevY}px` }, {}], defaultDuration);
|
|
276
|
+
});
|
|
277
|
+
hammer.on('pinchend', ({ scale }) => {
|
|
278
|
+
currentScale *= scale;
|
|
279
|
+
const prev = currentScale;
|
|
280
|
+
fitScale();
|
|
281
|
+
img.animate([{ scale: prev }, {}], defaultDuration);
|
|
282
|
+
});
|
|
283
|
+
handle(window, 'resize', resize);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function initImage() {
|
|
287
|
+
for (const image of $$<HTMLImageElement>('.image img')) {
|
|
288
|
+
handle(image, 'click', () => display(image));
|
|
289
|
+
}
|
|
290
|
+
}
|