mirror of
https://github.com/m1ngsama/FUJI.git
synced 2025-12-24 10:51:27 +00:00
Initial commit
This commit is contained in:
commit
644032463b
49 changed files with 17469 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
.vscode/
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
97
README.md
Normal file
97
README.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
## 👏🏻 Introduction
|
||||
|
||||
This is a minimalist, beautiful, responsive blogging program written in Astro.
|
||||
|
||||
## Preview
|
||||
|
||||
[https://astro-blog.qum.cc/](https://astro-blog.qum.cc/)
|
||||
|
||||
### Home
|
||||
|
||||

|
||||
|
||||
### Dark mode
|
||||
|
||||

|
||||
|
||||
### Normal article
|
||||
|
||||

|
||||
|
||||
### Syntax highlighting
|
||||
|
||||

|
||||
|
||||
### Three display model of images
|
||||
|
||||

|
||||
|
||||
The three display modes of images are: `wide`, `big`, `inline`.
|
||||
When you edit your markdown file, you can add `wide` or `big` or `inline` to the image alt, like this:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
<strong>The Separator is `|`, and the default mode is `big`.</strong>
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
In this Astro project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
|-- README.md
|
||||
|-- astro.config.mjs
|
||||
|-- package.json
|
||||
|-- public
|
||||
| |-- favicon.svg
|
||||
| `-- static
|
||||
|-- src
|
||||
| |-- components
|
||||
| | |-- BaseHead.astro // common <head> tags
|
||||
| | |-- Footer.astro
|
||||
| | |-- Header.astro
|
||||
| | `-- Navigation.astro
|
||||
| |-- consts.js
|
||||
| |-- env.d.ts
|
||||
| |-- layouts
|
||||
| | |-- BaseLayout.astro
|
||||
| | |-- MarkdownPost.astro
|
||||
| | |-- MoreTile.astro
|
||||
| | `-- Tile.astro
|
||||
| |-- pages
|
||||
| | |-- about.astro
|
||||
| | |-- archive.astro
|
||||
| | |-- index.astro
|
||||
| | |-- posts
|
||||
| | | |-- some markdown post.md // 这里写文章
|
||||
| | |-- rss.xml.js // RSS feed
|
||||
| | `-- tags
|
||||
| | `-- [tag].astro // dynamic route of all posts with a given tag
|
||||
| |-- styles
|
||||
| | `-- global.css // global styles
|
||||
| `-- utils.js
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :--------------------- | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
174
astro.config.mjs
Normal file
174
astro.config.mjs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import { visit } from 'unist-util-visit'
|
||||
import md5 from 'md5';
|
||||
|
||||
import { SITE_URL } from './src/consts';
|
||||
|
||||
|
||||
function pipeline() {
|
||||
return [
|
||||
|
||||
() => (tree) => {
|
||||
visit(tree, 'element', (node, index) => {
|
||||
if (node.tagName === 'p' && node.children[0].tagName === 'img') {
|
||||
node.tagName = 'figure';
|
||||
|
||||
let img = node.children[0];
|
||||
let sign = md5(img.properties.src);
|
||||
let data = img.properties.alt.split("|");
|
||||
let alt = data[0];
|
||||
let size = "big";
|
||||
if (data.length > 1) {
|
||||
size = data[1];
|
||||
}
|
||||
|
||||
let classes = ['image component image-fullbleed body-copy-wide nr-scroll-animation nr-scroll-animation--on'];
|
||||
classes.push(`image-${size}`);
|
||||
|
||||
node.properties.className = classes;
|
||||
node.children = [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['component-content'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['image-sharesheet'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: [`image image-load image-asset image-${sign}`], id: `lht${sign}` },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'picture',
|
||||
properties: { className: ['picture'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'img',
|
||||
properties: {
|
||||
'data-src': img.properties.src,
|
||||
alt: alt,
|
||||
className: ['picture-image'],
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['image-description'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['image-caption'] },
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: alt
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
() => (tree) => {
|
||||
tree.children.forEach((node) => {
|
||||
if (node.type === "raw") {
|
||||
node.value = `<div class="pagebody code component"><div class="component-content code"> ${node.value} </div></div>`
|
||||
// node.value = node.value.replace(/astro-code/g, 'astro-code')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
() => (tree) => {
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
let node = tree.children[i];
|
||||
if (node.type === "element" && ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName)) {
|
||||
|
||||
let next = tree.children[i + 1];
|
||||
let nodes = [node];
|
||||
while (next && !['figure'].includes(next.tagName) && next.type != "raw") {
|
||||
|
||||
nodes.push(next);
|
||||
next = tree.children[tree.children.indexOf(next) + 1];
|
||||
}
|
||||
|
||||
if (nodes.length > 1) {
|
||||
// rename label
|
||||
nodes.forEach((node) => {
|
||||
if (node.tagName === "p") {
|
||||
node.properties.className = ['pagebody-copy'];
|
||||
node.tagName = "div";
|
||||
}
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName)) {
|
||||
node.properties.className = ['pagebody-header'];
|
||||
}
|
||||
});
|
||||
|
||||
tree.children.splice(i, nodes.length, {
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['pagebody text component'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['component-content'] },
|
||||
children: nodes
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
() => (tree) => {
|
||||
let len = tree.children.length;
|
||||
for (let index = 0; index < len; index++) {
|
||||
let node = tree.children[index];
|
||||
if (node.type === "element" && node.tagName === "figure") {
|
||||
tree.children.splice(index, 0, {
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['tertiarynav component'] },
|
||||
children: [{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['component-content'] },
|
||||
}]
|
||||
})
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: SITE_URL,
|
||||
markdown: {
|
||||
rehypePlugins: pipeline(),
|
||||
syntaxHighlight: 'prism',
|
||||
},
|
||||
});
|
||||
6630
package-lock.json
generated
Normal file
6630
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "blog",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^2.1.1",
|
||||
"astro": "^2.0.11",
|
||||
"md5": "^2.3.0",
|
||||
"rehype": "^12.0.1",
|
||||
"unist-util-visit": "^4.1.2"
|
||||
}
|
||||
}
|
||||
13
public/favicon.svg
Normal file
13
public/favicon.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 873 B |
BIN
public/preview.png
Normal file
BIN
public/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 189 KiB |
10
public/static/css/github-dark.min.css
vendored
Normal file
10
public/static/css/github-dark.min.css
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
10
public/static/css/github.min.css
vendored
Normal file
10
public/static/css/github.min.css
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||
1
public/static/css/googlecode.min.css
vendored
Normal file
1
public/static/css/googlecode.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-comment,.hljs-quote{color:#800}.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-title{color:#008}.hljs-template-variable,.hljs-variable{color:#660}.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-string{color:#080}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-symbol{color:#066}.hljs-attr,.hljs-built_in,.hljs-doctag,.hljs-params,.hljs-title,.hljs-type{color:#606}.hljs-attribute,.hljs-subst{color:#000}.hljs-formula{background-color:#eee;font-style:italic}.hljs-selector-class,.hljs-selector-id{color:#9b703f}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-doctag,.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}
|
||||
13
public/static/css/stackoverflow-dark.min.css
vendored
Normal file
13
public/static/css/stackoverflow-dark.min.css
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: StackOverflow Dark
|
||||
Description: Dark theme as used on stackoverflow.com
|
||||
Author: stackoverflow.com
|
||||
Maintainer: @Hirse
|
||||
Website: https://github.com/StackExchange/Stacks
|
||||
License: MIT
|
||||
Updated: 2021-05-15
|
||||
|
||||
Updated for @stackoverflow/stacks v0.64.0
|
||||
Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less
|
||||
Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less
|
||||
*/.hljs{color:#fff;background:#1c1b1b}.hljs-subst{color:#fff}.hljs-comment{color:#999}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#88aece}.hljs-attribute{color:#c59bc1}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#f08d49}.hljs-selector-class{color:#88aece}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#b5bd68}.hljs-meta,.hljs-selector-pseudo{color:#88aece}.hljs-built_in,.hljs-literal,.hljs-title{color:#f08d49}.hljs-bullet,.hljs-code{color:#ccc}.hljs-meta .hljs-string{color:#b5bd68}.hljs-deletion{color:#de7176}.hljs-addition{color:#76c490}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
13
public/static/css/stackoverflow-light.min.css
vendored
Normal file
13
public/static/css/stackoverflow-light.min.css
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: StackOverflow Light
|
||||
Description: Light theme as used on stackoverflow.com
|
||||
Author: stackoverflow.com
|
||||
Maintainer: @Hirse
|
||||
Website: https://github.com/StackExchange/Stacks
|
||||
License: MIT
|
||||
Updated: 2021-05-15
|
||||
|
||||
Updated for @stackoverflow/stacks v0.64.0
|
||||
Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less
|
||||
Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less
|
||||
*/.hljs{color:#2f3337;background:#f6f6f6}.hljs-subst{color:#2f3337}.hljs-comment{color:#656e77}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#015692}.hljs-attribute{color:#803378}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#b75501}.hljs-selector-class{color:#015692}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#54790d}.hljs-meta,.hljs-selector-pseudo{color:#015692}.hljs-built_in,.hljs-literal,.hljs-title{color:#b75501}.hljs-bullet,.hljs-code{color:#535a60}.hljs-meta .hljs-string{color:#54790d}.hljs-deletion{color:#c02d2e}.hljs-addition{color:#2f6f44}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
1
public/static/css/vs2015.min.css
vendored
Normal file
1
public/static/css/vs2015.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#1e1e1e;color:#dcdcdc}.hljs-keyword,.hljs-literal,.hljs-name,.hljs-symbol{color:#569cd6}.hljs-link{color:#569cd6;text-decoration:underline}.hljs-built_in,.hljs-type{color:#4ec9b0}.hljs-class,.hljs-number{color:#b8d7a3}.hljs-meta .hljs-string,.hljs-string{color:#d69d85}.hljs-regexp,.hljs-template-tag{color:#9a5334}.hljs-formula,.hljs-function,.hljs-params,.hljs-subst,.hljs-title{color:#dcdcdc}.hljs-comment,.hljs-quote{color:#57a64a;font-style:italic}.hljs-doctag{color:#608b4e}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-tag{color:#9b9b9b}.hljs-template-variable,.hljs-variable{color:#bd63c5}.hljs-attr,.hljs-attribute{color:#9cdcfe}.hljs-section{color:gold}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-bullet,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag{color:#d7ba7d}.hljs-addition{background-color:#144212;display:inline-block;width:100%}.hljs-deletion{background-color:#600;display:inline-block;width:100%}
|
||||
82
public/static/js/animation.js
Normal file
82
public/static/js/animation.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
var animationElements = [];
|
||||
var imageElements = [];
|
||||
var animationElementName = ".small-load";
|
||||
|
||||
|
||||
// Hookable function
|
||||
var loadAnimation = function (item) {
|
||||
let img = new Image();
|
||||
img.src = item.children[0].children[0].dataset.src;
|
||||
img.onload = function () {
|
||||
item.classList.remove("small-load", "medium-load", "large-load");
|
||||
item.classList.add("small-loaded", "medium-loaded", "large-loaded");
|
||||
}
|
||||
}
|
||||
|
||||
// Hookable function
|
||||
var loadImage = function (index) {
|
||||
if (index >= imageElements.length) return;
|
||||
let item = imageElements[index];
|
||||
let image = new Image();
|
||||
item.src = item.dataset.src;
|
||||
image.src = item.src;
|
||||
image.onload = function () {
|
||||
loadImage(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initImage() {
|
||||
// get all the images with data-src attribute
|
||||
imageElements = document.querySelectorAll('img[data-src]')
|
||||
// load the images one by one
|
||||
loadImage(0);
|
||||
|
||||
|
||||
animationElements = document.querySelectorAll(animationElementName);
|
||||
// load the images which are in the viewport
|
||||
viewPortLoad(0);
|
||||
const debouncedHandleScroll = debounce(lazyAnimation, 10);
|
||||
// add the event listener
|
||||
window.addEventListener('scroll', debouncedHandleScroll);
|
||||
}
|
||||
|
||||
|
||||
function viewPortLoad(index) {
|
||||
if (index >= animationElements.length) return;
|
||||
let item = animationElements[index];
|
||||
if (!isElementInView(item)) {
|
||||
viewPortLoad(index + 1)
|
||||
return;
|
||||
};
|
||||
|
||||
loadAnimation(item)
|
||||
viewPortLoad(index + 1);
|
||||
}
|
||||
|
||||
|
||||
function lazyAnimation() {
|
||||
images = document.querySelectorAll(animationElementName);
|
||||
viewPortLoad(0);
|
||||
}
|
||||
|
||||
|
||||
// check if the element is in the viewport
|
||||
function isElementInView(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const elementTop = rect.top;
|
||||
const elementBottom = rect.bottom;
|
||||
return (elementTop >= 0 && elementBottom - 200 <= window.innerHeight);
|
||||
}
|
||||
|
||||
function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function () {
|
||||
let context = this;
|
||||
let args = arguments;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
fn.apply(context, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
879
public/static/js/hljs.js
Normal file
879
public/static/js/hljs.js
Normal file
File diff suppressed because one or more lines are too long
113
public/static/js/initPost.js
Normal file
113
public/static/js/initPost.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
|
||||
console.log("postInit.js loaded");
|
||||
var scriptMd5 = document.createElement("script");
|
||||
scriptMd5.src = "/static/js/md5.js";
|
||||
document.head.appendChild(scriptMd5);
|
||||
|
||||
scriptMd5.onload = function () {
|
||||
console.log("md5.js loaded")
|
||||
// step1. sythx highlighting
|
||||
syntaxHighlight();
|
||||
// step2. lazyload
|
||||
initLazyLoad();
|
||||
}
|
||||
|
||||
function initLazyLoad() {
|
||||
var script = document.createElement("script");
|
||||
script.src = "/static/js/animation.js";
|
||||
document.head.appendChild(script);
|
||||
|
||||
script.onload = function () {
|
||||
console.log("lazyload.js loaded");
|
||||
|
||||
animationElementName = ".image-load";
|
||||
|
||||
// Hook the loadImage function
|
||||
loadImage = (index) => {
|
||||
if (index >= imageElements.length) return;
|
||||
|
||||
let image = imageElements[index];
|
||||
image.src = image.dataset.src;
|
||||
let img = new Image();
|
||||
img.src = image.src;
|
||||
img.onload = function () {
|
||||
loadImage(index + 1);
|
||||
};
|
||||
}
|
||||
|
||||
loadAnimation = (item) => {
|
||||
let grandSon = item.firstChild.firstChild;
|
||||
let img = new Image();
|
||||
img.src = grandSon.src;
|
||||
let sign = md5(grandSon.src);
|
||||
|
||||
img.onload = function () {
|
||||
let percent = ((img.height / img.width) * 100).toFixed(5);
|
||||
var style = document.createElement("style");
|
||||
style.innerHTML = renderStyle(sign, percent);
|
||||
let target = document.getElementById(`lht${sign}`)
|
||||
|
||||
if (!target) return;
|
||||
target.parentNode.insertBefore(style, target);
|
||||
item.classList.remove("image-load");
|
||||
item.classList.add("image-loaded");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
initImage();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function renderStyle(sign, percent) {
|
||||
return `
|
||||
.image-${sign} {
|
||||
width: 100%;
|
||||
padding-top: ${percent}%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1068px) {
|
||||
.image-${sign} {
|
||||
width: 100%;
|
||||
padding-top: ${percent}%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 734px) {
|
||||
.image-${sign} {
|
||||
width: 100%;
|
||||
padding-top: ${percent}%;
|
||||
height: auto;
|
||||
}
|
||||
};`
|
||||
}
|
||||
|
||||
function syntaxHighlight() {
|
||||
var script = document.createElement("script");
|
||||
script.src = "/static/js/hljs.js";
|
||||
document.head.appendChild(script);
|
||||
|
||||
var styleLight = document.createElement("link");
|
||||
styleLight.rel = "stylesheet";
|
||||
styleLight.href = "/static/css/stackoverflow-light.min.css";
|
||||
|
||||
var styleDark = document.createElement("link");
|
||||
styleDark.rel = "stylesheet";
|
||||
styleDark.href = "/static/css/stackoverflow-dark.min.css";
|
||||
|
||||
if (document.querySelector("body").classList.contains("theme-dark")) {
|
||||
document.head.appendChild(styleDark);
|
||||
} else {
|
||||
document.head.appendChild(styleLight);
|
||||
}
|
||||
|
||||
script.onload = function () {
|
||||
console.log("hljs.js loaded");
|
||||
document.querySelectorAll("pre code").forEach(function (block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
};
|
||||
}
|
||||
184
public/static/js/md5.js
Normal file
184
public/static/js/md5.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
function md5cycle(x, k) {
|
||||
var a = x[0], b = x[1], c = x[2], d = x[3];
|
||||
|
||||
a = ff(a, b, c, d, k[0], 7, -680876936);
|
||||
d = ff(d, a, b, c, k[1], 12, -389564586);
|
||||
c = ff(c, d, a, b, k[2], 17, 606105819);
|
||||
b = ff(b, c, d, a, k[3], 22, -1044525330);
|
||||
a = ff(a, b, c, d, k[4], 7, -176418897);
|
||||
d = ff(d, a, b, c, k[5], 12, 1200080426);
|
||||
c = ff(c, d, a, b, k[6], 17, -1473231341);
|
||||
b = ff(b, c, d, a, k[7], 22, -45705983);
|
||||
a = ff(a, b, c, d, k[8], 7, 1770035416);
|
||||
d = ff(d, a, b, c, k[9], 12, -1958414417);
|
||||
c = ff(c, d, a, b, k[10], 17, -42063);
|
||||
b = ff(b, c, d, a, k[11], 22, -1990404162);
|
||||
a = ff(a, b, c, d, k[12], 7, 1804603682);
|
||||
d = ff(d, a, b, c, k[13], 12, -40341101);
|
||||
c = ff(c, d, a, b, k[14], 17, -1502002290);
|
||||
b = ff(b, c, d, a, k[15], 22, 1236535329);
|
||||
|
||||
a = gg(a, b, c, d, k[1], 5, -165796510);
|
||||
d = gg(d, a, b, c, k[6], 9, -1069501632);
|
||||
c = gg(c, d, a, b, k[11], 14, 643717713);
|
||||
b = gg(b, c, d, a, k[0], 20, -373897302);
|
||||
a = gg(a, b, c, d, k[5], 5, -701558691);
|
||||
d = gg(d, a, b, c, k[10], 9, 38016083);
|
||||
c = gg(c, d, a, b, k[15], 14, -660478335);
|
||||
b = gg(b, c, d, a, k[4], 20, -405537848);
|
||||
a = gg(a, b, c, d, k[9], 5, 568446438);
|
||||
d = gg(d, a, b, c, k[14], 9, -1019803690);
|
||||
c = gg(c, d, a, b, k[3], 14, -187363961);
|
||||
b = gg(b, c, d, a, k[8], 20, 1163531501);
|
||||
a = gg(a, b, c, d, k[13], 5, -1444681467);
|
||||
d = gg(d, a, b, c, k[2], 9, -51403784);
|
||||
c = gg(c, d, a, b, k[7], 14, 1735328473);
|
||||
b = gg(b, c, d, a, k[12], 20, -1926607734);
|
||||
|
||||
a = hh(a, b, c, d, k[5], 4, -378558);
|
||||
d = hh(d, a, b, c, k[8], 11, -2022574463);
|
||||
c = hh(c, d, a, b, k[11], 16, 1839030562);
|
||||
b = hh(b, c, d, a, k[14], 23, -35309556);
|
||||
a = hh(a, b, c, d, k[1], 4, -1530992060);
|
||||
d = hh(d, a, b, c, k[4], 11, 1272893353);
|
||||
c = hh(c, d, a, b, k[7], 16, -155497632);
|
||||
b = hh(b, c, d, a, k[10], 23, -1094730640);
|
||||
a = hh(a, b, c, d, k[13], 4, 681279174);
|
||||
d = hh(d, a, b, c, k[0], 11, -358537222);
|
||||
c = hh(c, d, a, b, k[3], 16, -722521979);
|
||||
b = hh(b, c, d, a, k[6], 23, 76029189);
|
||||
a = hh(a, b, c, d, k[9], 4, -640364487);
|
||||
d = hh(d, a, b, c, k[12], 11, -421815835);
|
||||
c = hh(c, d, a, b, k[15], 16, 530742520);
|
||||
b = hh(b, c, d, a, k[2], 23, -995338651);
|
||||
|
||||
a = ii(a, b, c, d, k[0], 6, -198630844);
|
||||
d = ii(d, a, b, c, k[7], 10, 1126891415);
|
||||
c = ii(c, d, a, b, k[14], 15, -1416354905);
|
||||
b = ii(b, c, d, a, k[5], 21, -57434055);
|
||||
a = ii(a, b, c, d, k[12], 6, 1700485571);
|
||||
d = ii(d, a, b, c, k[3], 10, -1894986606);
|
||||
c = ii(c, d, a, b, k[10], 15, -1051523);
|
||||
b = ii(b, c, d, a, k[1], 21, -2054922799);
|
||||
a = ii(a, b, c, d, k[8], 6, 1873313359);
|
||||
d = ii(d, a, b, c, k[15], 10, -30611744);
|
||||
c = ii(c, d, a, b, k[6], 15, -1560198380);
|
||||
b = ii(b, c, d, a, k[13], 21, 1309151649);
|
||||
a = ii(a, b, c, d, k[4], 6, -145523070);
|
||||
d = ii(d, a, b, c, k[11], 10, -1120210379);
|
||||
c = ii(c, d, a, b, k[2], 15, 718787259);
|
||||
b = ii(b, c, d, a, k[9], 21, -343485551);
|
||||
|
||||
x[0] = add32(a, x[0]);
|
||||
x[1] = add32(b, x[1]);
|
||||
x[2] = add32(c, x[2]);
|
||||
x[3] = add32(d, x[3]);
|
||||
|
||||
}
|
||||
|
||||
function cmn(q, a, b, x, s, t) {
|
||||
a = add32(add32(a, q), add32(x, t));
|
||||
return add32((a << s) | (a >>> (32 - s)), b);
|
||||
}
|
||||
|
||||
function ff(a, b, c, d, x, s, t) {
|
||||
return cmn((b & c) | ((~b) & d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
function gg(a, b, c, d, x, s, t) {
|
||||
return cmn((b & d) | (c & (~d)), a, b, x, s, t);
|
||||
}
|
||||
|
||||
function hh(a, b, c, d, x, s, t) {
|
||||
return cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
}
|
||||
|
||||
function ii(a, b, c, d, x, s, t) {
|
||||
return cmn(c ^ (b | (~d)), a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md51(s) {
|
||||
txt = '';
|
||||
var n = s.length,
|
||||
state = [1732584193, -271733879, -1732584194, 271733878], i;
|
||||
for (i=64; i<=s.length; i+=64) {
|
||||
md5cycle(state, md5blk(s.substring(i-64, i)));
|
||||
}
|
||||
s = s.substring(i-64);
|
||||
var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0];
|
||||
for (i=0; i<s.length; i++)
|
||||
tail[i>>2] |= s.charCodeAt(i) << ((i%4) << 3);
|
||||
tail[i>>2] |= 0x80 << ((i%4) << 3);
|
||||
if (i > 55) {
|
||||
md5cycle(state, tail);
|
||||
for (i=0; i<16; i++) tail[i] = 0;
|
||||
}
|
||||
tail[14] = n*8;
|
||||
md5cycle(state, tail);
|
||||
return state;
|
||||
}
|
||||
|
||||
/* there needs to be support for Unicode here,
|
||||
* unless we pretend that we can redefine the MD-5
|
||||
* algorithm for multi-byte characters (perhaps
|
||||
* by adding every four 16-bit characters and
|
||||
* shortening the sum to 32 bits). Otherwise
|
||||
* I suggest performing MD-5 as if every character
|
||||
* was two bytes--e.g., 0040 0025 = @%--but then
|
||||
* how will an ordinary MD-5 sum be matched?
|
||||
* There is no way to standardize text to something
|
||||
* like UTF-8 before transformation; speed cost is
|
||||
* utterly prohibitive. The JavaScript standard
|
||||
* itself needs to look at this: it should start
|
||||
* providing access to strings as preformed UTF-8
|
||||
* 8-bit unsigned value arrays.
|
||||
*/
|
||||
function md5blk(s) { /* I figured global was faster. */
|
||||
var md5blks = [], i; /* Andy King said do it this way. */
|
||||
for (i=0; i<64; i+=4) {
|
||||
md5blks[i>>2] = s.charCodeAt(i)
|
||||
+ (s.charCodeAt(i+1) << 8)
|
||||
+ (s.charCodeAt(i+2) << 16)
|
||||
+ (s.charCodeAt(i+3) << 24);
|
||||
}
|
||||
return md5blks;
|
||||
}
|
||||
|
||||
var hex_chr = '0123456789abcdef'.split('');
|
||||
|
||||
function rhex(n)
|
||||
{
|
||||
var s='', j=0;
|
||||
for(; j<4; j++)
|
||||
s += hex_chr[(n >> (j * 8 + 4)) & 0x0F]
|
||||
+ hex_chr[(n >> (j * 8)) & 0x0F];
|
||||
return s;
|
||||
}
|
||||
|
||||
function hex(x) {
|
||||
for (var i=0; i<x.length; i++)
|
||||
x[i] = rhex(x[i]);
|
||||
return x.join('');
|
||||
}
|
||||
|
||||
function md5(s) {
|
||||
return hex(md51(s));
|
||||
}
|
||||
|
||||
/* this function is much faster,
|
||||
so if possible we use it. Some IEs
|
||||
are the only ones I know of that
|
||||
need the idiotic second function,
|
||||
generated by an if clause. */
|
||||
|
||||
function add32(a, b) {
|
||||
return (a + b) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') {
|
||||
function add32(x, y) {
|
||||
var lsw = (x & 0xFFFF) + (y & 0xFFFF),
|
||||
msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xFFFF);
|
||||
}
|
||||
}
|
||||
33
src/components/BaseHead.astro
Normal file
33
src/components/BaseHead.astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import "../styles/global.css";
|
||||
const { title, description, image = "/preview.png" } = Astro.props;
|
||||
import { SITE_URL } from "../consts";
|
||||
const { pathname } = Astro.url;
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={`${SITE_URL}${pathname}`} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={new URL(image, `${SITE_URL}${pathname}`)} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={`${SITE_URL}${pathname}`} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={new URL(image, `${SITE_URL}${pathname}`)} />
|
||||
47
src/components/Footer.astro
Normal file
47
src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
import { SITE_TITLE, SITE_EMAIL } from "../consts";
|
||||
const { theme } = Astro.props;
|
||||
import { SITE_NAME } from "../consts";
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
---
|
||||
|
||||
<div class:list={["footer-main", { "footer-dark": theme === "dark" }]}>
|
||||
<div class="content-body footer-wraper">
|
||||
<div class="footer-box">
|
||||
<div class="foot-nav">
|
||||
<div class="foot-nav-items">
|
||||
<div class="item">
|
||||
<div class="logo">{SITE_TITLE}</div>
|
||||
<div class="email">Email: {SITE_EMAIL}</div>
|
||||
</div>
|
||||
|
||||
<div class="item products">
|
||||
<div class="item-title">作品</div>
|
||||
<a href="/" target="_blank">本站</a>
|
||||
<a href="https://the.top" target="_blank">TOP Link</a>
|
||||
<a href="https://news.the.top" target="_blank">TOP News</a>
|
||||
</div>
|
||||
|
||||
<div class="item community">
|
||||
<div class="item-title">社媒</div>
|
||||
<a href="https://twitter.com/austinit" target="_blank">Twitter</a>
|
||||
<a href="https://github.com/austin2035" target="_blank">Github</a>
|
||||
<a href="https://t.me/austin2035" target="_blank">Telegram</a>
|
||||
</div>
|
||||
|
||||
<div class="item resources">
|
||||
<div class="item-title">友链</div>
|
||||
<a href="https://the.top">THE.TOP</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="copyright">
|
||||
© {`2018-${year} ${SITE_NAME}`}
|
||||
<a href="//github.com/austin2035/astro-air-blog">astro-air-blog</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
7
src/components/Header.astro
Normal file
7
src/components/Header.astro
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Navigation from "./Navigation.astro";
|
||||
---
|
||||
|
||||
<header>
|
||||
<Navigation />
|
||||
</header>
|
||||
25
src/components/Navigation.astro
Normal file
25
src/components/Navigation.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import { SITE_TITLE } from "../consts";
|
||||
---
|
||||
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-wrapper">
|
||||
<div class="nav-content-wrapper">
|
||||
<div class="nav-content">
|
||||
<a href="/" class="nav-title">{SITE_TITLE}</a>
|
||||
<div class="nav-menu">
|
||||
<div class="nav-item-wrapper">
|
||||
<a href="/archive" class="nav-item-content">目录</a>
|
||||
</div>
|
||||
<div class="nav-item-wrapper">
|
||||
<a href="/about" class="nav-item-content">关于</a>
|
||||
</div>
|
||||
<div class="nav-item-wrapper">
|
||||
<a href="/rss.xml" class="nav-item-content" target="_blank">RSS</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
5
src/consts.js
Normal file
5
src/consts.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const SITE_TITLE = `Austin's Blog`;
|
||||
export const SITE_DESCRIPTION = 'Austin Site Description';
|
||||
export const SITE_EMAIL = 'no.sql@qq.com'
|
||||
export const SITE_NAME = 'astro-blog.qum.cc';
|
||||
export const SITE_URL = "https://astro-blog.qum.cc";
|
||||
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="astro/client" />
|
||||
20
src/layouts/ArchivePostList.astro
Normal file
20
src/layouts/ArchivePostList.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import { formatDateV2 } from "../utils";
|
||||
const { posts } = Astro.props;
|
||||
posts.sort((a, b) => Date.parse(b.frontmatter.pubDate) - Date.parse(a.frontmatter.pubDate));
|
||||
---
|
||||
|
||||
<ul>
|
||||
{
|
||||
posts.map((post) => {
|
||||
return (
|
||||
<li>
|
||||
<a href={post.url} class="tag_post-content">
|
||||
<span class="tag-date">[{formatDateV2(post.frontmatter.pubDate)}]</span>
|
||||
{post.frontmatter.title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
19
src/layouts/BaseLayout.astro
Normal file
19
src/layouts/BaseLayout.astro
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
import BaseHead from "../components/BaseHead.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
|
||||
const { primaryTitle } = Astro.props;
|
||||
const title = primaryTitle ? `${primaryTitle} - ${SITE_TITLE}` : SITE_TITLE;
|
||||
---
|
||||
|
||||
<html class="js no-touch progressive-image no-reduced-motion progressive" lang="zh-CN" dir="ltr">
|
||||
<head>
|
||||
<BaseHead title={title} description={SITE_DESCRIPTION} />
|
||||
</head>
|
||||
<body class="page-landing">
|
||||
<Header />
|
||||
<slot />
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
76
src/layouts/MarkdownPost.astro
Normal file
76
src/layouts/MarkdownPost.astro
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
import BaseHead from "../components/BaseHead.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
|
||||
import { formatDate } from "../utils";
|
||||
import { SITE_TITLE } from "../consts";
|
||||
const { frontmatter } = Astro.props;
|
||||
const type = frontmatter.tags[0];
|
||||
const { pubDate, title, description, featured } = frontmatter;
|
||||
const dateFormated = formatDate(pubDate);
|
||||
---
|
||||
|
||||
<html lang="zh-CN" dir="ltr" class="js no-touch progressive-image no-reduced-motion progressive">
|
||||
<head>
|
||||
<BaseHead title={`${title} - ${SITE_TITLE}`} description={description} image={frontmatter.cover.square} />
|
||||
</head>
|
||||
<body class:list={["page-article", { "theme-dark": frontmatter.theme === "dark" }]}>
|
||||
<Header />
|
||||
<main id="main" class="main">
|
||||
<section>
|
||||
<article class="article">
|
||||
<div class:list={[{ "featured-header": featured, "article-header": !featured }]}>
|
||||
<div class="category component">
|
||||
<div class="component-content">
|
||||
<div class="category-eyebrow">
|
||||
<span class="category-eyebrow__category category_original">{type}</span>
|
||||
<span class="category-eyebrow__date">{dateFormated}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagetitle component">
|
||||
<div class="component-content">
|
||||
<h1 class="hero-headline">{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class:list={[{ "featured-subhead": featured, "article-subhead": !featured }, "component"]}>
|
||||
<div class="component-content">{description}</div>
|
||||
</div>
|
||||
|
||||
<div class:list={["tagssheet component"]}>
|
||||
<div class="component-content">
|
||||
{
|
||||
frontmatter.tags.map((tag) => {
|
||||
return (
|
||||
<a href={`/tags/${tag}`} class="tag">
|
||||
{tag}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
<div class="component">
|
||||
<div class="component-content">
|
||||
<div class="article-copyright">
|
||||
<a class="content" href="https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh" target="_blank"
|
||||
>版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)</a
|
||||
>
|
||||
<p class="content">作者: {frontmatter.author} 发表日期:{dateFormated}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<script is:inline>
|
||||
var script = document.createElement("script");
|
||||
script.src = "/static/js/initPost.js";
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
31
src/layouts/MoreTile.astro
Normal file
31
src/layouts/MoreTile.astro
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
import { formatDate } from "../utils";
|
||||
const { title, href, cover, tags, date } = Astro.props;
|
||||
|
||||
const dateFormated = formatDate(date);
|
||||
const type = tags[0];
|
||||
const label = `${title} - ${type} - 发表时间 ${dateFormated}`;
|
||||
---
|
||||
|
||||
<li
|
||||
role="listitem"
|
||||
class="tile-item item-list nr-scroll-animation"
|
||||
style="--nr-animation-transform-y:20%;"
|
||||
>
|
||||
<a
|
||||
href={href}
|
||||
class="tile tile-list medium-load small-load large-load"
|
||||
aria-label={label}
|
||||
>
|
||||
<div class="tile__media" aria-hidden="true">
|
||||
<img class="cover image" data-src={cover} alt="lt"/>
|
||||
</div>
|
||||
<div class="tile__description" aria-hidden="true">
|
||||
<div class="tile__head">
|
||||
<div class="tile__category">{type}</div>
|
||||
<div class="tile__headline">{title}</div>
|
||||
</div>
|
||||
<div class="tile__timestamp icon-hide icon icon-before icon-clock">{dateFormated}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
35
src/layouts/Tile.astro
Normal file
35
src/layouts/Tile.astro
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
import { formatDate } from "../utils";
|
||||
const { title, href, cover, tags, date, level} = Astro.props;
|
||||
|
||||
const dateFormated = formatDate(date);
|
||||
const type = tags[0];
|
||||
const label = `${title} - ${type} - 发表时间 ${dateFormated}`;
|
||||
// level 1: hreo
|
||||
// level 2: 2up
|
||||
// level 3: 3up
|
||||
---
|
||||
|
||||
<li
|
||||
role="listitem"
|
||||
class:list={["tile-item", "nr-scroll-animation", { "item-hero": level === "1", "item-2up": level === "2", "item-3up": level === "3" }]}
|
||||
style="--nr-animation-transform-y:20%;"
|
||||
>
|
||||
<a
|
||||
href={href}
|
||||
class:list={["tile", "large-load", "medium-load", "small-load", { "tile-hero": level === "1", "tile-2up": level === "2", "tile-3up": level === "3" }]}
|
||||
aria-label={label}
|
||||
>
|
||||
<div class="tile__media" aria-hidden="true">
|
||||
<img class="cover image" data-src={cover} alt="lt"/>
|
||||
</div>
|
||||
|
||||
<div class="tile__description" aria-hidden="true">
|
||||
<div class="tile__head">
|
||||
<div class="tile__category">{type}</div>
|
||||
<div class="tile__headline">{title}</div>
|
||||
</div>
|
||||
<div class="tile__timestamp icon-hide icon icon-before icon-clock">{dateFormated}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
15
src/pages/404.astro
Normal file
15
src/pages/404.astro
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import ArchivePostList from "../layouts/ArchivePostList.astro";
|
||||
---
|
||||
|
||||
<BaseLayout primaryTitle="404 Not Found">
|
||||
<section class="archive">
|
||||
<div class="section-content section-tag">
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">404 Not Found</h2>
|
||||
<div class="tag-post-list">来到了一片荒原,这里什么都没有。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
57
src/pages/about.astro
Normal file
57
src/pages/about.astro
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
---
|
||||
|
||||
<BaseLayout primaryTitle="关于">
|
||||
<section class="archive">
|
||||
<div class="section-content section-tag">
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">关于我</h2>
|
||||
<div class="tag-post-list">
|
||||
<ul>
|
||||
<li>爱看老高</li>
|
||||
<li>爱折腾</li>
|
||||
<li>爱音乐</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">关注我</h2>
|
||||
<div class="tag-post-list">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://twitter.com/austinit" target="_blank"> Twitter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/austin2035" target="_blank">Github</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">随想</h2>
|
||||
<div class="tag-post-list">
|
||||
<ul>
|
||||
<li>我常常将人看做机器,他输出了什么,常常取决于他输入了什么。</li>
|
||||
<li>屏蔽垃圾输入,寻求优质信息源, 才能输出优质信息。</li>
|
||||
<li>看到这,你已经被我的观念输入一遍了。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">一些作品</h2>
|
||||
<div class="tag-post-list">
|
||||
<ul>
|
||||
<li>
|
||||
有点多,我下次整理一下。
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
29
src/pages/archive.astro
Normal file
29
src/pages/archive.astro
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import ArchivePostList from "../layouts/ArchivePostList.astro";
|
||||
const allPosts = await Astro.glob("./posts/*.md");
|
||||
const tags = ["新闻稿", "虚幻引擎", "源码研究"];
|
||||
const posts = [];
|
||||
|
||||
tags.forEach((tag) => {
|
||||
let filteredPosts = allPosts.filter((post) => post.frontmatter.tags.includes(tag));
|
||||
posts.push(filteredPosts);
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout primaryTitle="归档">
|
||||
<section class="archive">
|
||||
<div class="section-content section-tag">
|
||||
{
|
||||
tags.map((tag, index) => {
|
||||
return (
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">{tag}</h2>
|
||||
<div class="tag-post-list">{posts[index].length !== 0 ? <ArchivePostList posts={posts[index]} /> : <div class="no-posts">暂无文章</div>}</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
79
src/pages/index.astro
Normal file
79
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import Tile from "../layouts/Tile.astro";
|
||||
import MoreTile from "../layouts/MoreTile.astro";
|
||||
const allPosts = await Astro.glob("../pages/posts/*.md");
|
||||
allPosts.sort((a, b) => Date.parse(b.frontmatter.pubDate) - Date.parse(a.frontmatter.pubDate));
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<section class="everydayfeed">
|
||||
<div class="section-content">
|
||||
<h2 class="section-head">最新文章</h2>
|
||||
<ul role="list" class="section-tiles">
|
||||
{
|
||||
// tile-hero
|
||||
allPosts.slice(0, 1).map((post) => {
|
||||
return (
|
||||
<Tile title={post.frontmatter.title} href={post.url} date={post.frontmatter.pubDate} tags={post.frontmatter.tags} cover={post.frontmatter.cover.url} level="1" />
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
// tile-2up
|
||||
allPosts.slice(1, 5).map((post) => {
|
||||
return (
|
||||
<Tile title={post.frontmatter.title} href={post.url} date={post.frontmatter.pubDate} tags={post.frontmatter.tags} cover={post.frontmatter.cover.url} level="2" />
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
// tile-3up
|
||||
allPosts.slice(5, 11).map((post) => {
|
||||
return (
|
||||
<Tile title={post.frontmatter.title} href={post.url} date={post.frontmatter.pubDate} tags={post.frontmatter.tags} cover={post.frontmatter.cover.url} level="3" />
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="more-from-newsroom">
|
||||
<div class="section-content">
|
||||
<h2 class="section-head">更多文章</h2>
|
||||
|
||||
|
||||
<ul role="list" class="section-tiles">
|
||||
|
||||
{
|
||||
// tile-2up
|
||||
allPosts.slice(0, 6).map((post) => {
|
||||
return (
|
||||
<MoreTile title={post.frontmatter.title} href={post.url} date={post.frontmatter.pubDate} tags={post.frontmatter.tags} cover={post.frontmatter.cover.url} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<div class="view-archive-wrapper">
|
||||
<a href="/archive" class="cta-primary-light" data-analytics-region="router" data-analytics-title="view archive">阅读历史文章</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var script = document.createElement("script");
|
||||
script.src = "/static/js/animation.js";
|
||||
document.head.appendChild(script);
|
||||
|
||||
script.onload = function () {
|
||||
console.log("lazyload.js loaded");
|
||||
// when layout is loaded, load the images
|
||||
initImage();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</BaseLayout>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: 'Apple 推出新款 HomePod,带来突破性音质与智能体验'
|
||||
pubDate: 2035-03-25
|
||||
description: '呈现出类拔萃的音质、增强的 Siri 功能以及安全放心的智能家居体验'
|
||||
author: 'Apple Newsroom'
|
||||
cover:
|
||||
url: 'https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-hero-230118_big.jpg.large_2x.jpg'
|
||||
square: 'https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-hero-230118_big.jpg.large_2x.jpg'
|
||||
alt: 'cover'
|
||||
tags: ["新闻稿", "Apple", "HomePod"]
|
||||
theme: 'light'
|
||||
featured: true
|
||||
---
|
||||
|
||||

|
||||
|
||||
加利福尼亚州,库比提诺Apple 今日宣布推出 HomePod(第二代)。这款功能强大的智能扬声器采用优美的标志性设计,为用户带来新一代声学体验。HomePod 汇集了多项 Apple 创新技术与 Siri 智能,提供先进计算音频技术,支持播放沉浸式空间音频曲目,呈现前所未有的聆听体验。HomePod 带来管理日常任务、控制智能家居的便利新方式,用户可以使用 Siri 创建智能家居自动化功能,在家中触发烟雾或一氧化碳警报时获得通知,或者查看房间的温度与湿度——以上操作不必动手就能完成。
|
||||
新款 HomePod 从今日起可在线或通过 Apple Store app 订购,2 月 3 日(周五)起正式发售。
|
||||
|
||||
“利用我们的专业音频技术和创新,新款 HomePod 可呈现醇厚深沉的低音、自然的中音和清澈细腻的高音。”Apple 全球市场营销高级副总裁 Greg Joswiak 表示,“随着 HomePod mini 大受欢迎,我们看到用户对体积更大、声学表现更强劲的 HomePod 兴趣也与日俱增。我们很高兴能为全球各地的顾客带来新一代 HomePod。”
|
||||
|
||||
## 优美设计
|
||||
|
||||
新款 HomePod 的外观由无缝透声织网和背光触控板构成,优美的设计与各种空间相得益彰。HomePod 提供白色与全新的午夜色两种外观,后者由 100% 再生织物构成,配有同色系编织电源线。
|
||||
|
||||

|
||||
|
||||
## 强劲的声学表现
|
||||
|
||||
HomePod 呈现出类拔萃的音质,低音醇厚深沉,高频惊艳动人。定制研发的高振幅低音单元、振幅高达惊人的 20 毫米的强劲电机驱动振膜、内置低音均衡器麦克风、底部环绕着由 5 个波束成形高音单元组成的阵列,共同打造强大的声学体验。S7 芯片结合软件和系统感应技术,提供更加先进的计算音频,彻底发挥声学系统的全部潜能,呈现前所未有的聆听体验。
|
||||
|
||||

|
||||
|
||||
利用室内空间感应技术,HomePod 可识别附近表面反射的声音,判断是否靠近墙壁等表面,并对音频进行实时调整。5 个高音单元组成的波束成形阵列利用精确指向控制分离与定向传送直达声和环境声,让用户在清澈的人声和醇美的器乐声中尽情沉醉。
|
||||
|
||||
用户可聆听 Apple Music 曲库中的数千万首歌曲1,通过单个 HomePod 或用两个 HomePod 组成立体声组合享受空间音频。用户可以通过 Siri 了解各种音乐知识,并根据艺人、歌曲、歌词、年代、类型、心情或活动搜索音乐。
|
||||
|
||||
多个 HomePod 带来更出色的体验 两个及更多 HomePod 或 HomePod mini 可解锁一系列强大功能。利用支持多房间音频的隔空播放技术2,用户只要说“嘿 Siri”,或者触碰并按住 HomePod 顶部,就能用多个 HomePod 播放同一首歌曲,或在不同 HomePod 上播放不同歌曲,甚至使用多个 HomePod 进行广播,传话到各个房间。
|
||||
用户还可以把两个 HomePod 放在一起,组成立体声组合3。立体声组合不但可以分离左右声道,还能和谐地播放双声道音频,营造比传统立体声扬声器更宽广、更具沉浸感的声场,呈现出类拔萃的聆听体验。
|
||||
|
||||

|
||||
|
||||
## 无缝整合 Apple 生态系统
|
||||
|
||||
利用超宽带技术,用户可以将 iPhone 上播放的任何声音,如喜爱的歌曲、播客甚至通话直接转移到 HomePod4。房间中的任何人都可以拿着 iPhone 靠近 HomePod,播放建议会自动出现在 iPhone 屏幕上,从而轻松控制音频播放或获取个性化歌曲与播客推荐。HomePod 还能识别最多 6 个用户的语音,让所有家庭成员都能轻松聆听## 私人歌单、设置提醒和日历事项。
|
||||
|
||||
HomePod 的“查找”功能可以让遗落的设备播放声音,帮用户定位 iPhone 等 Apple 设备。用户还能向 Siri 询问好友或家人的位置(需要对方在“查找”app 中选择共享自己的位置)。
|
||||
|
||||

|
||||
|
||||
## 智能家居必备
|
||||
|
||||
依托声音识别功能5,HomePod 可以听到烟雾或一氧化碳警报声,识别出此类声音后,直接向用户的 iPhone 发送通知。全新的内置温度和湿度感应器可衡量室内环境,让用户设置自动化操作,例如房间内到达一定温度时关上窗帘,或者打开风扇。
|
||||
|
||||
通过 Siri,顾客无需动手就能控制单一设备,或者创建场景,例如让多个智能家居设备在早上同时开始工作,或者设置反复出现的自动化操作,例如“嘿 Siri,每天早晨日出时打开窗帘6”。当用户通过 Siri 操控暖气等未能以肉眼分辨是否已开关的配件,或者操控位于不同房间的配件时,HomePod 会发出新的确认音效。经过重制的海洋、森林、雨声等环境音效将进一步整合到体验中,让顾客可以为场景、自动化和警报添加新的声音。
|
||||
|
||||
用户也可以在重新设计的家庭 app 中直观地操控、查看和管理配件。家庭 app 提供了“环境”、“灯”和“安全”等新类别,并提供全新多机位视图,让用户轻松设置和控制智能家居。
|
||||
|
||||

|
||||
|
||||
## Matter 支持
|
||||
|
||||
去年秋季推出的 Matter 连接标准确保智能家居产品在跨生态系统工作时保持最高级别的安全。Apple 是 Connectivity Standards Alliance(连接标准联盟)成员,该联盟与其他行业领导者一起维护 Matter 智能家居连接标准。HomePod 可连接并支持 Matter 的配件,担任家居中枢,让用户出门在外也能远程控制。
|
||||
|
||||
## 顾客数据属于私有财产
|
||||
|
||||
保护顾客隐私始终是 Apple 的一项核心价值观。所有智能家居设备之间的通信始终保持端到端加密,包括 HomeKit 安防视频摄像头录制的内容,均无法被 Apple 读取。使用 Siri 时,请求音频默认不会被存储。这些功能确保让用户居家隐私得到保障。
|
||||
|
||||
## HomePod 与环境
|
||||
|
||||
HomePod 致力于最大程度地降低对环境的影响,在多个印刷电路板的电镀层中使用 100% 再生金(对 HomePod 而言尚属首次),且在扬声器磁体中使用 100% 再生稀土元素。HomePod 符合 Apple 对能效的严苛标准,且不含汞、溴化阻燃剂(BFR)、聚氯乙烯(PVC)和铍。经重新设计的包装材料不再使用塑料外膜,96% 的包装材料采用纤维基,让 Apple 更加接近 2025 年底前在包装中完全去除塑料的目标。
|
||||
|
||||
目前 Apple 已实现全球公司运营碳中和,并计划到 2030 年实现全部供应链和所有产品生命周期 100% 碳中和。这意味着每一部售出的 Apple 设备,从零件制造、组装、运输、用户使用、充电,直到设备和材料回收的所有环节,都将实现净零气候影响。
|
||||
|
||||
## 价格与上市时间
|
||||
|
||||
HomePod(第二代)售价为 RMB 2299 (中国大陆),今日起通过 apple.com.cn/store 及 Apple Store app 对澳大利亚、加拿大、中国大陆、法国、德国、意大利、日本、西班牙、英国、美国及其他 11 个国家和地区的顾客开放订购,并于 2 月 3 日(周五)起正式发售。
|
||||
HomePod(第二代)支持运行 iOS 16.3 或后续系统的 iPhone SE(第二代)及后续机型或 iPhone 8 及后续机型;运行 iPadOS 16.3 的 iPad Pro、iPad(第五代)及后续机型、iPad Air(第三代)及后续机型或 iPad mini(第五代)及后续机型。
|
||||
购买 HomePod 的新用户,即可获享 6 个月 Apple Music 免费订阅服务。
|
||||
|
||||
## 关于 Apple
|
||||
|
||||
Apple 于 1984 年推出 Macintosh,为个人技术带来了巨大变革。今天,Apple 凭借 iPhone、iPad、Mac、Apple Watch 和 Apple TV 引领全球创新。Apple 的 5 个软件平台,iOS、iPadOS、macOS、watchOS 和 tvOS,带来所有 Apple 设备之间的顺畅使用体验,同时以 App Store、Apple Music、Apple Pay 和 iCloud 等突破性服务赋予人们更大的能力。Apple 的 100,000 多名员工致力于打造全球顶尖的产品,并让世界更加美好。
|
||||
|
||||
## Apple Music 需要订阅
|
||||
|
||||
多房间音频需要多个 HomePod 或支持隔空播放并运行最新版本隔空播放软件的扬声器。
|
||||
组建 HomePod 立体声组合需要两个相同型号的 HomePod 扬声器,例如两个 HomePod mini,两个 HomePod(第二代)或两个 HomePod(第一代)。
|
||||
在 iPhone 上使用接力功能需运行 iOS 16.3。
|
||||
|
||||
声音识别功能会在今春稍晚通过软件更新推出。声音识别功能可以探测烟雾和一氧化碳警报声,并在识别后向用户发送通知。当用户身处可能受到伤害的环境中,或在高风险或紧急情况下,均不应依赖声音识别功能。声音识别功能需要更新版家庭架构,该架构将在家庭 app 的独立更新中推出。它要求所有连接家居配件的 Apple 设备均使用最新版本软件。智能家居配件需单独购买。
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: 'Apple 推出为头脑风暴和创意协作设计的全新强大 app 无边记'
|
||||
pubDate: 2035-04-01
|
||||
description: 'iPhone、iPad 及 Mac 版无边记让视觉化协作更轻松。'
|
||||
author: 'Apple Newsroom'
|
||||
cover:
|
||||
url: 'https://www.apple.com.cn/newsroom/images/product/apps/standard/Apple-Freeform-hero_big.jpg.large_2x.jpg'
|
||||
square: 'https://www.apple.com.cn/newsroom/images/product/apps/standard/Apple-Freeform-hero_big.jpg.large_2x.jpg'
|
||||
alt: 'cover'
|
||||
tags: ["新闻稿", "Apple", "无边记"]
|
||||
theme: 'light'
|
||||
featured: true
|
||||
---
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
无边记是一款全新 app,今日正式推出,包括 iOS、iPadOS 和 macOS 最新版本。无边记帮助用户在灵活多用的画布上管理并以视觉方式展示内容,在同一个地方查看、共享与协作,而不必考虑排版或页面大小问题。用户无需离开画板,即可添加不同类型的文件并实时预览。无边记为协作而设计,让用户更轻松地邀请他人在同一块白板上工作还能在 FaceTime 通话期间与他人协作。无边记白板存储在 iCloud 上,用户可以在不同设备间同步内容。
|
||||
|
||||
“无边记为 iPhone、iPad 和 Mac 用户进行视觉化协作开启了无限可能。”Apple 全球产品营销副总裁 Bob Borchers 表示,“无边记带来了无边无际的画布,支持上传多种不同类型的文件,集成了 iCloud,还提供了诸多协作功能,为用户随时随地创建头脑风暴的共享空间。”
|
||||
|
||||
## 为创意设计的画布与易用工具
|
||||
|
||||
无边记为用户提供了绝佳的白板体验,在同一个地方汇聚灵感创意。无边无际的画布会随着内容添加到白板上而不断扩展,让用户在处理很多文件或与他人协作时获得无限灵活性。通过内置手势支持,用户可以在白板各处流畅转移。
|
||||
|
||||
这款 app 提供了多种画笔类型和颜色选项,供用户描绘创意、添加注释、绘制图表。iPhone 与 iPad 用户可以在画布上的任意地方用手指作画;无边记还支持 Apple Pencil,让用户更轻松地随时随地在 iPad 上描绘灵感。
|
||||
|
||||

|
||||
|
||||
iPad Pro 上的无边记中展示着随手写笔记和一系列绘画工具。
|
||||
无论使用随手写记笔记、用绘画工具画图、用蜡笔或填充工具填色,无边记用户都能让创意进入新境界。
|
||||
无边记支持大量文件类型,包括照片、视频、音频、文档、PDF、网站链接和地图地点链接、便签、图形、图表等等。用户还可以利用 iPhone 和 iPad 摄像头直接在白板中插入图片或扫描文档。无边记还提供了全面的图形素材库,包括 700 多种可选图形,用户可以改变这些图形的颜色和大小、添加文本,甚至创建个性化图形。
|
||||
|
||||
用户可以将内容从文件和访达等 app 拖放至画板,并通过内置对齐指导轻松保持画板整洁有序。用户只需轻点两下,不必离开画板即可通过速览预览内容,还能同时播放多个视频文件,创建动态视图。图片和 PDF 等内容可以在画板上锁定位置,协作者可以在对象上方或周围添加注释,这意味着无边记极为适合用来为建筑平面图或家装计划描绘灵感,或者供教练在篮球战术板上布置战术。
|
||||
|
||||
iPad Pro 的无边记画板支持多种类型的文件。
|
||||
无边记画板支持多种类型的文件,用户无需离开画板即可实时预览,特别适合为学校项目汇集文档或者规划下一次旅行。
|
||||
|
||||

|
||||
|
||||
## 协作空间
|
||||
|
||||
无论用户在办公室或是出门在外,无论是独立工作还是与他人协作,无边记都能派上大用场。无边记支持多人在同一块白板上协作,为集体项目或与好友规划度假方案创造共享创意空间。
|
||||
|
||||
利用信息 app 的全新协作功能,用户只需将无边记画板拖入信息对话串就能邀请他人在白板上展开协作。对话串的所有成员都会自动被邀请加入画板,马上开始协作。有人编辑内容后,活动更新会出现在信息对话串顶部。
|
||||
|
||||
这款 app 内置了 FaceTime 通话功能,用户使用无边记时,轻点屏幕右上角的协作按钮,即可发起 FaceTime 通话。依托快速同步功能和 iCloud 集成,所有协作者都可以查看其他人添加的内容或者改动。无边记白板会在 iPhone、iPad 和 Mac 间同步,用户可以通过链接或电子邮件邀请他人,还能以 PDF 格式导出画板,或者截取屏幕。
|
||||
|
||||
iPad Pro 上,无边记正在使用 FaceTime 通话协作功能。
|
||||
依托 FaceTime 和 iCloud 集成,无边记为协作而设计,帮助用户更轻松地邀请他人在画板上共同工作。
|
||||
|
||||

|
||||
|
||||
## 推出时间
|
||||
|
||||
今日起,所有支持 iOS 16.2、iPad OS 16.2 或 macOS Ventura 13.1 的 iPhone、iPad 及 Mac 均可免费下载无边记。
|
||||
22
src/pages/posts/apple-reports-first-quarter-results.md
Normal file
22
src/pages/posts/apple-reports-first-quarter-results.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: 'Apple 公布第一季度业绩'
|
||||
pubDate: 2035-03-01
|
||||
description: '活跃设备现有使用量突破 20 亿大关,在所有主要产品类别均创下历史新高'
|
||||
author: 'Apple Newsroom'
|
||||
cover:
|
||||
url: 'https://www.apple.com.cn/newsroom/images/apple-logo_black.jpg.landing-regular_2x.jpg'
|
||||
square: 'https://www.apple.com.cn/newsroom/images/apple-logo_black.jpg.landing-regular_2x.jpg'
|
||||
alt: 'cover'
|
||||
tags: ["新闻稿", "Apple", "财报"]
|
||||
theme: 'light'
|
||||
featured: false
|
||||
---
|
||||
|
||||
加利福尼亚州,库比提诺 Apple 今日发布了截至 2022 年 12 月 31 日的 2023 财年第一季度财务业绩。公司公布本季度营收达到 1172 亿美元,同比下降 5%。本季度稀释后每股收益为 1.88 美元。
|
||||
|
||||
“在继续应对挑战性环境的同时,我们很自豪能推出迄今最出色的产品和服务。与以往一样,我们继续致力于放眼长远,在一切行动中贯彻我们的价值观。”Apple CEO Tim Cook 表示, “在 12 月季度,我们达成了一项里程碑式的成就:我们很高兴地宣布,我们的活跃设备现有使用量已经突破 20 亿大关。”
|
||||
|
||||
“我们的服务业务营收创下历史新高,达 208 亿美元。尽管宏观经济环境不景气且供应严重受限,按固定汇率计算的公司营收仍然有所增长。”Apple CFO Luca Maestri 表示,“我们本季度创造了 340 亿美元的经营现金流,向股东返还了超过 250 亿美元,并继续投资支持我们的长期增长计划。”
|
||||
|
||||
Apple 董事会已宣布派发每股 0.23 美元的公司普通股现金股息。股息将于 2023 年 2 月 16 日派发给所有在 2023 年 2 月 13 日收市时已登记在册的股东。
|
||||
88
src/pages/posts/apple.md
Normal file
88
src/pages/posts/apple.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: '从农场到海洋:保育红树林,维系当地人生计,保护地球 '
|
||||
pubDate: 2035-07-01
|
||||
description: 'Apple 与 Applied Environmental Research Foundation 合作,将促进印度马哈拉施特拉邦红树林的保育工作'
|
||||
author: 'Apple Newsroom'
|
||||
cover:
|
||||
url: 'https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Alibaug-canoe_Full-Bleed-Image.jpg.large_2x.jpg'
|
||||
square: 'https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Alibaug-canoe_Full-Bleed-Image.jpg.large_2x.jpg'
|
||||
alt: 'cover'
|
||||
tags: ["特写", "环保", "Apple", "印度", "红树林", "保育", "新闻稿"]
|
||||
theme: 'dark'
|
||||
featured: true
|
||||
---
|
||||
|
||||

|
||||
|
||||
在马哈拉施特拉邦繁华的滨海城市孟买以南仅约 96 公里的地方,出现了一个截然不同的世界。繁华都市的摩天大厦、餐厅、酒店、购物区、不计其数的“嘟嘟车”与现代汽车逐渐消失,未铺装的道路、棕榈树、山羊、拉车的牛、小型露天市场和路边餐馆出现在视野里。
|
||||
|
||||
Raigad 县的 Alibaug 连接了孟买与通向阿拉伯海的河网。海岸地区长有 21000 公顷红树林。红树林是地球最为天然的守护者之一,能够抵御气候变化带来的种种影响,包括突如其来的暴雨、海潮上升、热带气旋或飓风,甚至海啸。同时,红树林还能起到碳汇的作用,吸收大气中的二氧化碳,并将其存储在土壤、植物和其他沉积物中,这一机制被称为“蓝碳”。
|
||||
|
||||
Applied Environmental Research Foundation(AERF)在 2021 年获得了 Apple 的资助。该组织正在这一地区进行探索,计划通过在当地社区中创设可持续的替代行业,来培育红树林生态系统的生物多样性与适应能力并从中受益,从而保护红树林的未来。保护协议将向当地村民提供持续性支持,以换取对土地的保护,并促进当地经济转型,使之有赖于保持红树林的完好与健康。
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
AERF 还将从保护国际基金会(Conservation International)2018 年启动的哥伦比亚 Cispatá 试行蓝碳项目获得的经验应用到印度的红树林。
|
||||
|
||||
“对抗气候变化,是在为全球各地受这场危机影响最严重的社区而奋斗。从哥伦比亚到肯尼亚,再到菲律宾,人们的生活和生计饱受气候变化的威胁,这也正是我们工作的重点。”Apple 环境、政策与社会事务副总裁 Lisa Jackson 表示,“我们在印度的新的合作项目延续了这一努力,帮助当地社区从红树林的保育中收获经济利益,并抵御气候变化的恶劣影响。”
|
||||
|
||||
AERF 主席 Archana Godbole 从小就热爱大自然。“植物代表了年龄与时间,”她表示,“而树木则代表了耐心。它们是时间的无声观众——我越是研究树木、了解树木,就越在它们面前感到渺小。我的经历让我逐渐意识到,我想为保护和拯救树木与森林而工作。”
|
||||
|
||||

|
||||
|
||||
|
||||
专业植物分类学家 Godbole 在过去三十年间一直致力于以社区为基础的环保工作。在 Raigad 县,AERF 正在寻求与当地社区达成环保协议。由于海水侵袭和人造堤坝的损毁,当地居民的作物与农田的损失惨重。
|
||||
|
||||
“这里世代居住的人们本是农民,但突然之间海洋来到了他们的门口。”Godbole 说道,“人们学会了新的技巧,来应对新的局面。现在我们已经知道红树林在应对气候变化与碳封存方面作用非常重要。我们很高兴能来到这里,努力与当地居民合作,让红树林为他们带来更多福祉。我们满怀信心地期待,他们能够在心中建立与土地和红树林的深厚联系。”
|
||||
|
||||
下面,我们可以看到这些村庄的面貌,以及被很多人称之为家的社区面对日益严峻的环境灾害所表现出的适应能力。
|
||||
|
||||

|
||||
|
||||
Karanjveera 是一个小型内陆村庄,也是许多农民和渔民及其家人的家园。当地渔民通常以捕捞小虾蟹为主。Namdev Waitaram More 是村里的长者,也是传统捕鱼技术的行家。75 岁高龄的 More 一生都与红树林和谐共处,并深深尊敬它们抵御海水侵袭稻田的保护能力。
|
||||
|
||||
More 正与表弟帮助其他社区成员与 AERF 联系,探讨村中盐沼和红树林保育问题。“红树林就像海绵。”他说道,“人们与这里的红树林关系密切。如果红树林没了,我们的堤坝就没了,我们的稻田也就没了。因为我们通过食物、堤坝和红树林相连,我们才得以生存。”
|
||||
|
||||

|
||||
|
||||
Usha 和儿子 Tushar Thakur 都是 Hashiware 的农民。这个村庄位于 Amba 河畔,自从本地堤坝于 1990 年溃决后,这里的农田就一直淹没在海水之下。这里现已被红树林覆盖,但往日的残留依稀可见,遭弃置的房屋矗立在泥水中,距离河岸仅有几米。Thakur 是首批与 AERF 签订红树林保护协议的村民之一。
|
||||
从 1996 年开始,曾经属于 Hashiware 村农民的农田就已被红树林覆盖。
|
||||
|
||||
“通过我们的工作与对红树林重要性的警示,以及创造可持续创收活动的机会,我们为 Raigad 县的沿海社区带来了希望。”AERF 的 Godbole 介绍道。
|
||||
|
||||

|
||||
|
||||
红树林保护印度沿海村庄的能力在近年得到了展现。2004 年,印尼海底强烈地震引发的多重海啸对印度东海岸造成了冲击。从那时起,人们意识到红树林是本地社区的无声守护者,吸收着巨浪的震撼,保护着岸边的村庄。在过去几年间,强热带气旋越来越频繁地侵袭这一地区,包括 2020 年的 Nisarga 和 2021 年的 Tauktae。在 Raigad 县,村民们正在努力保护红树林,从而保护自己的生计与福祉。
|
||||
|
||||

|
||||
|
||||
在 Ganesh Patti 村,农民们同意各人负责一部分堤坝的维护,将农田与红树林和河岸分离。但个人的维护工作还不够。当地渔民 Mangesh Patil 的家现已毁坏,被红树林所围绕。海浪和越来越高的海潮逐渐毁掉了他的房子。
|
||||
但在这个已消失的村落的村民看来,一切似乎发生在一夜之间。
|
||||
|
||||
“一天晚上,大家都睡了,夜潮来了,突然之间海水侵入,我们的床铺都湿透了。第二天早晨,我们意识到,整个村子都被淹了。”
|
||||
潮水退去后,居民们清点了土地和生计的损失——他们明白,自己必须从零开始了。迁移到邻近的村庄后,Patil 和他的兄弟等许多人都还是决定继续返回自己的老家,回到自己的印度教寺庙,在曾是儿时家园的水中捕鱼捞蟹。
|
||||
|
||||
“无论大自然给人类什么样的条件,我们都应该学会生存。”Patil 说道,“我们就是这样做的。现在,我们与这些红树林之间已经建立了联系。这里是我们出生的地方,我们在这里很开心。所以我们总是会回到这里。”
|
||||
|
||||

|
||||
|
||||
在与当地村庄签署环保协议提供资金支持的同时,Apple 的拨款还将用于购买与分发便携生物质炉具,让人们不再需要砍伐红树林,也能生火做饭。
|
||||
|
||||

|
||||
|
||||
|
||||
来自 Pen Vashi 的渔民 Bhavik Patil 十分熟悉与红树林相关的生计,他帮助 AERF 在 Raigad 县的村庄中与村民探讨相关问题。出生在渔业家庭的 Patil 记得自己小时候,父母会在下河工作前,在红树上绑上秋千,让他和兄弟玩耍。现在,在捕鱼捞蟹之余,他也和很多人一起,与 Mothe Bhal 和 Vithalwadi 等村庄的村民就保护和可持续利用红树林的问题展开磋商。为了保护红树林,他和同伴请求村民收集从树木上自然掉落的干燥树枝。
|
||||
|
||||
|
||||

|
||||
|
||||
对 AERF 的许多成员来说,保护红树林不仅仅是一项工作,更是他们的激情所在。Godbole 和联合创始人 Jayant Sarnaik 27 年前创建了这个组织,并持续致力于通过当地人参与的方式实现环保使命。
|
||||
|
||||
“对于生活在近海地区的社区来说,构建抵御气候变化的适应能力是一项持续工作。”AERF 的 Sarnaik 表示,“这些社区在海岸生活了很长的时间,他们非常了解海洋,以及海洋和气候的关系。气候变化对他们来说并不是新鲜事;然而,他们在过去 5 到 10 年间也经历了剧烈的变化。近年的热带气旋让这里的人们认识到了红树林的重要性。面对这些灾害,红树林是最强有力的自然防线。同时,它也在更广泛的范围内唤醒了人们对于红树林重要意义的认识。
|
||||
|
||||
正如 Godbole 所言,未来值得期待。“与 Apple 和保护国际基金会的合作是一次绝佳的机会,探索红树林保育和社区福祉的互惠互利。”她表示。“虽然红树林保育问题在各地有所差异,但在我们的项目所在地,也存在着很多机遇。培训满怀热情的年轻团队和本地社区关于蓝碳的知识,定会帮助我们在阿拉伯海沿岸这一充满生机的地区实现红树林保育方面的长足进展。”
|
||||
|
||||
Apple 致力于帮助全球各地受气候变化冲击最严重的社区培育适应能力,获取经济收益。去年,公司与保护国际基金会合作,支持创办了同类首创的“无法复原的碳金融实验室”,旨在保护全球最脆弱的生态系统,并与中国绿色碳汇基金会合作,资助相关研究及试行项目,在中国建立更多天然碳汇。此外,在世界地球日当周,顾客每次通过 apple.com、Apple Store app 或在 Apple Store 零售店使用 Apple Pay 购物,Apple 都将向世界自然基金会(WWF)捐出 1 美元。通过此举,Apple 向 WWF 的 Climate Crowd 项目提供支持。该项目旨在促进社区对环境变化的适应能力与可持续生计。
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: 'UE5.1新功能在《堡垒之夜:大逃杀》第四章中的实战测试'
|
||||
pubDate: 2035-05-01
|
||||
description: '在这篇博客文章中,我们将着眼于一些虚幻引擎5功能,它们得益于《堡垒之夜:大逃杀》第四章的检验,已经变得更加完善了;我们还据此对UE 5.1做出了改进,这将帮助你创建更加精致和快速的开放世界游戏。'
|
||||
author: '虚幻引擎官网'
|
||||
cover:
|
||||
url: 'https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-header-1920x1080-2e96869442d6.jpg?resize=1&w=1920'
|
||||
square: 'https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-header-1920x1080-2e96869442d6.jpg?resize=1&w=1920'
|
||||
alt: 'cover'
|
||||
tags: ["功能", "虚幻引擎", "游戏"]
|
||||
theme: 'dark'
|
||||
featured: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
|
||||
十二月初,当《堡垒之夜:大逃杀》第四章第一赛季发布时,玩家很难不注意到视觉保真度和细节得到了质的提升。这并不是巧合,通过虚幻引擎5.1,游戏的最新章节利用了虚幻引擎5最具创新性的全新功能,如Lumen、Nanite、虚拟阴影贴图和时序超级分辨率等等。
|
||||
|
||||
《堡垒之夜:大逃杀》第四章出色地展示了Epic Games“吃自家狗粮”的决心,我们在真正的游戏开发压力下对新功能做出了实战测试。这一举措促使我们改进了在UE 5.0中引入的创新功能集,它们随后被交付给了《堡垒之夜》团队使用。
|
||||
|
||||
我们之前通过《黑客帝国觉醒:虚幻引擎5体验》对UE5中的功能进行了测试,在最新一代的主机上以30FPS的帧率展示了一座逼真的城市。在《堡垒之夜:大逃杀》第四章中,开发团队面临着新出现的挑战:确保游戏在所有平台上以60FPS的帧率运行,同时在以建设和破坏为特色的大型动态开放世界中提供当今玩家所期望的质量——高标准的风格化画面、精致的植被以及昼夜循环。
|
||||
|
||||
在这篇博客文章中,我们将着眼于一些在第四章中得到检验并因此获益良多的功能。我们还会提供一些深层次文章的链接,它们更加详细地介绍了我们对Lumen、Nanite和虚拟阴影贴图的改进,探讨了我们从《堡垒之夜:大逃杀》第四章的工作中所汲取的经验,我们相信这些经验将有助于你使用虚幻引擎5创建更加精致、快速的开放世界游戏。
|
||||
|
||||
## Lumen
|
||||
|
||||
Lumen是虚幻引擎5中新引入的实时全局光照和反射解决方案。Lumen使用硬件和软件光线追踪提供逼真的间接反射光照、反射和阴影,它们能够对直接光照和几何体的变化做出动态反应。举例而言,你可以通过Lumen实现:一面蓝色的墙,为附近所有物体或角色带来一丝蓝色色调;黄金时刻的阳光穿过一扇窗户,在没有任何局部光源的情况下照亮整间房屋;当一扇外门被打开时,光线进入房间。
|
||||
|
||||
我们针对《堡垒之夜:大逃杀》第四章,在虚幻引擎中改进了Lumen的许多方面。我们优化了整个系统,使我们能够在支持Lumen的平台上实现60FPS的帧率,同时极大地提高间接光照和反射的质量。
|
||||
|
||||
《堡垒之夜》的世界充满了树木和草地,这是一片理想的试验场,让我们能够改进Lumen间接光照在这类环境中的质量。
|
||||
|
||||

|
||||
|
||||
|
||||
## Nanite
|
||||
Nanite彻底改变了美术师和设计师构建虚拟世界的方式,它在突破几何细节极限的同时也简化了创作过程。美术师不再需要考虑网格体LOD或给定对象的正确多边形数量,即可实现最佳视觉质量和性能。
|
||||
|
||||
在UE 5.0中,Nanite出色地实现了不透明的刚性几何体,《堡垒之夜》的城市环境充分利用了这一点。无论镜头拉得多近,经过建模的砖块、木材和窗户装饰都能展示复杂的细节。
|
||||
|
||||
从UE 5.1开始,Nanite支持了全局位置偏移和遮罩材质,《堡垒之夜》的美术师能够在极精致的动画树木和树叶上使用这项技术。树叶和草叶被建模成了随风自然摇曳的几何体。为了确保在任意渲染距离都能保留所有重要的茂盛细节,我们还引入了一种让Nanite简化植被几何体的新方法。
|
||||
|
||||

|
||||
|
||||
## 虚拟阴影贴图
|
||||
|
||||
虚拟阴影贴图(VSM)与Nanite相结合,将美术师在《堡垒之夜:大逃杀》第四章中添加的所有复杂几何细节凸显了出来。与传统的级联阴影贴图相比,VSM最大的优势在于,无论物体是近在玩家眼前,还是远在地平线上,它都能提供接近像素级精度的阴影细节。
|
||||
|
||||
新的《堡垒之夜》几何体为我们带来了一个难题:我们需要进一步改进VSM,使其与植被及当日时间动态系统相配合——对于我们之前为提高性能而采用的许多阴影缓存方案来说,此二者成了一道阻碍。当然,这一切都必须符合60FPS的预算。
|
||||
|
||||
《堡垒之夜》现在也在室内使用了许多较小的局部光源,如电灯或天花板顶灯。VSM缓存在这些地方表现出色。像这样大量使用局部光源让我们有机会在UE 5.1中进一步提高VMS的性能。我们可以通过“单通道投射”选项单次批量化处理许多光源的阴影更新,从而提高GPU的利用率。我们还建立了启发式方法,将远距离光源的阴影更新分摊到多个帧中。
|
||||
|
||||

|
||||
|
||||
## 局部曝光
|
||||
Lumen允许美术师创造出对比度极高的场景,例如,在非常暗的房间中透过窗户看到明亮的室外,或者在同一视图中,房屋表面被明亮的天空和太阳照亮,而室内几乎漆黑一片。对于这种视图,单一的曝光值效果不佳:明亮区域最终会出现过度曝光,而黑暗区域则会近乎全黑,无法看清。
|
||||
|
||||
UE5中的曝光系统针对《堡垒之夜:大逃杀》第四章做出了改进,提供了更多工具让美术师实现其所需的美术方向,同时也带来了更出色的性能。美术师现在可以分别控制局部曝光在高光和阴影中的应用,这有助于在高动态范围光照条件下保留细节。除此之外,局部曝光与后期处理管线中的其他部分(尤其是泛光和镜头光斑)更加深入地整合到了一起,使结果更加一致。
|
||||
|
||||
我们还新支持了在忽略材质属性的同时,基于照度计算自动曝光。这有助于材质在不同的光照条件下保持外观的一致,并提高游戏过程中的图像稳定性。
|
||||
|
||||
## 时序超级分辨率
|
||||
时序超级分辨率(TSR)通过将一些成本高昂的渲染计算分摊到多个帧中,使我们能够大幅削减渲染漂亮4K图像的成本。考虑到《堡垒之夜:大逃杀》第四章60FPS的目标,我们需要对TSR做出许多性能和质量上的优化。因此,UE 5.1中的TSR比我们在UE 5.0中发布的版本更好,也更快。我们还改进了TSR依据配置文件(低级、中级、高级和史诗级)对性能和质量的调整范围。
|
||||
|
||||
我们会在即将发布的一篇博客文章中深入探讨这些改进的技术细节。
|
||||
|
||||
## 云层
|
||||
为了配合《堡垒之夜:大逃杀》第四章中视觉保真度的飞跃,我们改进了虚幻引擎5.1中云层渲染的质量。为了满足严格的性能预算,UE5在内部会以较低的分辨率渲染云层,并对其进行向上采样。以前,这会导致遮挡了背景云层的前景网格体周围产生不完整的边缘,致使明亮的天空颜色在边缘处渗透出来。在UE 5.1中,我们可以更好地解决全分辨率下云层向上采样的问题,不会出现这种渗透。我们还改进了当网格体从云层前穿行而过时,对云层的时序重建工作,有效地减少了因移除遮挡而产生的尾迹。
|
||||
|
||||

|
||||
|
||||
## Niagara
|
||||
《堡垒之夜》利用Niagara视效系统创造了由美术师主导的程序化破坏效果。Niagara能够对游戏做出反应,每当武器击中目标时,都能生成并准确模拟飞溅的碎片。
|
||||
|
||||
《堡垒之夜:大逃杀》第四章中新推出的高保真火焰使用基于体素的传播机制,更好地表现了火焰在表面和地形上的蔓延,同时还支持在载具及物理对象间传播火焰。为了正确放置火焰,并将其对准到燃烧表面,该系统使用了在引擎内部经过烘焙的Niagara流体模拟,并将其应用到了Niagara粒子中(数据读取自火焰游戏系统)。
|
||||
|
||||

|
||||
|
||||
## 对世界构建的支持
|
||||
随着第四章的发布,《堡垒之夜:大逃杀》现在在每个平台上都使用了UE5最新的世界构建和自动流送解决方案。其中包括世界分区、数据层、关卡实例、自动HLOD以及一Actor一文件。这套工具集有助于建立更具协作性的工作流程,并提升关卡设计师和美术师构建世界的效率。
|
||||
|
||||
与《堡垒之夜》之前版本中使用的自定义流送解决方案相比,目前方案最大的一个不同之处是,新的世界分区系统实现了自动化,开发者需要付出的时间更少了,同时,HLOD的过渡也更加稳定,提供了更好的整体游戏体验。
|
||||
|
||||
第四章包含超过10万个Actor文件,为了更有效地处理如此之多的文件,我们改进了变更列表窗口,增加了新的用户体验和新的非受控变更列表类型,并将它与场景大纲视图和主视口更好地集成在了一起。
|
||||
|
||||
为了给第四章提供支持,我们做的进一步改进是在寻路网格体(静态和动态)的生成功能中添加对世界分区的支持。这意味着你可以加载一个世界单元,为其生成寻路网格体,将其序列化,然后卸载该单元并继续处理下一单元。
|
||||
|
||||
在UE 5.1中,所有开发者都可以使用这些经过改进的功能,并且,随着《堡垒之夜》团队不断对这些功能展开实战测试,我们可以期待在未来版本中看到进一步的改进。
|
||||
|
||||

|
||||
|
||||
|
||||
## 智能对象和状态树
|
||||
智能对象允许AI在关卡中选择一个对象或区域,并动态注入行为。相关的数据和配置(如动画)会被存储在对象中,而不是代理中,这实现了大量代理之间的共享,促成了更高效的内存利用和更出色的性能,无需编辑核心行为即可更灵活地扩展默认行为。
|
||||
|
||||
状态树是一种可扩展的通用状态机,它在灵活的决策树结构(以及直观而紧凑的用户界面)中结合了经典的状态机机制,同时保留了高性能。
|
||||
|
||||
这两项技术在UE 5.0版本中是作为实验功能发布的,但经过《堡垒之夜:大逃杀》第四章中的成功部署后,它们在5.1版本中已进入生产就绪状态。在第四章之前,智能对象只被用于让AI播放简单的动画或表情。通过在智能对象流程中整合状态树,我们成功地实现了更复杂、更丰富的机器人行为,例如机器人与《堡垒之夜》篝火互动的方式。
|
||||
|
||||

|
||||
|
||||
|
||||
## 更高的开发者效率
|
||||
我们一直在寻找提高开发者效率的方法,让他们可以专注于创作过程。《堡垒之夜》采用了新的图形技术,如磁盘空间占用量更大的Nanite,这为我们带来了一项挑战:寻找方法减少团队加载游戏或从Perforce获取内容的时间。
|
||||
|
||||
庞大且分布于多个地点的《堡垒之夜》团队从新的虚幻云DDC中获益匪浅,这是一个为派生数据缓存建立的全球高效模型,允许团队成员快速访问纹理或网格体等内容的优化版本,消除了在本地进行处理的需要。因此,《堡垒之夜》中的编辑器加载时间至少快了2倍。
|
||||
|
||||
虚拟资产减少了内容的磁盘占用量,从而将《堡垒之夜》单个工作空间的同步时间缩短了四倍以上;通过删除多个工作空间共用的重复数据,我们甚至还可以节省更多磁盘空间和同步时间。我们从虚幻资产较小的类属性(如纹理分辨率和格式等)中剥离出了原始纹理像素等大体积数据,并将它们分开存储。从表面看,虚拟资产与普通虚幻资产并没有区别,但是它们要小很多倍。开发者的工作流程几乎无需做出改变,而客户端只在需要时才会下载要在编辑器中使用的数据,显著减少了等待Perforce进行同步的时间。
|
||||
|
||||
## 机器学习
|
||||
虚幻引擎的机器学习(ML)变形器允许你使用自定义的Maya插件训练将在虚幻引擎中实时运行的机器学习模型,为复杂的专有绑定(或任意变形)创建高保真的近似模拟。
|
||||
|
||||
在第四章中,《堡垒之夜》团队使用ML变形器实现了更高质量的角色肌肉和布料变形,Hulk和Sunlit是第一批从中受益的角色。团队能够通过外部DCC工具捕获复杂的变形和模拟,并将几何结果注入它们的游戏绑定。这次实战测试的结果是,ML变形器在UE 5.1中从实验版升级到了测试版,同时,团队与ML变形器的工程师展开了密切合作,使该技术有机会得到进一步完善,他们计划在UE 5.2中增加新功能和稳定性修复。
|
||||
|
||||
## MetaHuman框架
|
||||
在第四章中,《堡垒之夜》团队与3Lateral及MetaHuman团队的开发者密切合作,升级了《堡垒之夜》角色的面部动画功能。其中包括一些对角色质量的更新,例如增加了新角色的表情和关节数量,创建了新的绑定和动画工具,将面部动画推向了新的高度。
|
||||
|
||||
因此,虚幻引擎5.1改进了绑定逻辑的运行时功能,将性能开销控制在了约束范围内,还专门创建了重映射节点,为《堡垒之夜》中MetaHuman技术的向前和向后兼容性提供了支持。
|
||||
|
||||
我们希望你喜欢我们在《堡垒之夜:大逃杀》第四章的开发过程中对虚幻引擎5.1做出的所有改进,并发现它们有助于你将游戏的视觉保真度提升到新水平。我们期待看到结果!
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: '用 iPhone 14 拍摄:摄影佳作激发创作灵感,定格精彩人像、自然与城市风光'
|
||||
pubDate: 2035-09-01
|
||||
description: ''
|
||||
author: 'Apple Newsroom'
|
||||
cover:
|
||||
url: 'https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_iPhone-14-Pro-Max-with-the-Main-Camera-by-Xiaobei-Fuzhou_12192022_Full-Bleed-Image.jpg.xlarge_2x.jpg'
|
||||
square: 'https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_iPhone-14-Pro-Max-with-the-Main-Camera-by-Xiaobei-Fuzhou_12192022_Full-Bleed-Image.jpg.xlarge_2x.jpg'
|
||||
alt: 'cover'
|
||||
tags: ["新闻稿", "Apple", "iPhone 14", "摄影"]
|
||||
theme: 'dark'
|
||||
featured: true
|
||||
---
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
通过 iPhone 14 Pro Max,iPhone 14 Pro,iPhone 14 以及 iPhone 14 Plus 上突破性创新的摄像头系统,中国的摄影师们拍摄并分享令人惊艳的照片和视频,以此激励更多 iPhone 用户在即将到来的节日假期中捕捉亮丽的城市景观、迷人的自然风光,并记录下与亲人相聚的难忘时刻。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
iPhone 14 和 iPhone 14 Plus 为摄影和摄像设立了新标准,搭载具备更大感光元件和像素尺寸的 1200 万像素主摄、首次配备自动对焦功能并采用 ƒ/1.9 光圈的全新前置原深感摄像头,以及超广角摄像头用于拍摄更宽广的场景。除此之外,光像引擎也带来低光表现的巨大跃升。
|
||||
|
||||
通过软硬件深度集成,光像引擎能够提升所有摄像头在中低光环境下的照片表现:超广角摄像头表现提升可达 2 倍,原深感摄像头表现提升可达 2 倍,全新主摄表现提升可达 2.5 倍。iPhone 14 系列还推出全新的运动模式——即使在大幅的抖动和运动中,也能轻松拍摄无比丝滑的视频。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
iPhone 14 Pro 和 iPhone 14 Pro Max 上搭载的 Pro 级摄像头系统挑战 iPhone 影像的能力极限。iPhone 14 Pro 首次采用搭载四合一像素传感器的全新 4800 万像素主摄,同时配备光像引擎使细节的丰富程度达到前所未有的高度。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
从日常到专业使用,iPhone 14 Pro 上推出的摄像头系统将为每一位用户赋能,令他们的照片和视频更出彩。全新 4800 万像素主摄采用四合一像素传感器,可根据拍摄照片进行调整,还具备第二代传感器位移式光学图像防抖功能,全面提升图像质量。这有利于复杂光线环境下的拍摄,尤其在低光条件下也能拍摄到优质的影像。此外,用户现可以用 4800 万像素拍摄 ProRAW 影像,利用每一个像素,为专业用户实现全新的创意工作流。
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
加上超广角和长焦摄像头一起,iPhone 14 Pro 现在支持 0.5 倍、1 倍、2 倍和 3 倍共计四个变焦选项 ,使用户无需牺牲画面质量,即可在拍摄照片和视频时拥有更多取景选项。丰富的焦距范围可以轻松应对多种场景,足之所至,即成佳作。四合一像素传感器还支持 2 倍长焦选项,利用该传感器正中的 1200 万像素拍摄全分辨率照片和 4K 视频,无需数码变焦。该选项能够以常用焦距带来光学品质成像,对于人像模式等功能来说非常实用。
|
||||
|
||||
|
||||

|
||||
|
||||
强大的 A16 仿生芯片和配备 5 核图形处理器的 A15 仿生芯片,是驱动强大功能的幕后功臣,如运动模式让摄像师轻松实现更加稳定、丝滑的视频画面,电影效果模式带来沉浸式叙事风格。
|
||||
|
||||
全新的运动模式可拍摄出无比丝滑的视频,应对大幅的抖动、位移和震动,甚至在运动中拍摄时也不例外,让用户在手持拍摄时无需携带云台等外设。该模式支持高达 2.8K 60 fps 视频,而增强的电影效果模式现已支持 4K 分辨率和 24 fps 的电影帧率录制,让用户能够通过生动的叙事释放无穷的创造力,并与全世界分享他们通过 iPhone 所看到的内容。
|
||||
|
||||
自适应原彩闪光灯经过全面重新设计,采用九粒 LED 灯珠阵列,会根据用户选择的照片焦距更改闪光模式,使拍摄对象始终处于最佳光线中。与上一代相比,全新闪光灯亮度提升可达 2 倍,光线均匀度提升可达 3 倍。
|
||||
此外,全新原深感摄像头配备更快的光圈,让低光条件下照片视频更美丽细致,并支持自动对焦功能。
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
iPhone 14 系列的所有摄像头均支持夜间模式,这意味着当用户在深夜拍摄风景或在低光条件下自拍时,照片会被神奇地点亮。同时,夜间模式现将广角摄像头的想象力提升到全新水平,得益于更大的感光元件和更快的光圈,使 iPhone 14 和 iPhone 14 Plus 上的曝光时间最快提升可达 2 倍。
|
||||
|
||||

|
||||
453
src/pages/posts/golang.md
Normal file
453
src/pages/posts/golang.md
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: 'Golang net/http & HTTP Serve 源码分析'
|
||||
pubDate: 2035-06-01
|
||||
description: '很多Go web框架都通过封装 net/http 来实现核心功能,因此学习 net/http 是研究 Gin等框架的基础。'
|
||||
author: 'Austin'
|
||||
cover:
|
||||
url: 'https://lookcos.cn/usr/uploads/2022/04/2067928922.png'
|
||||
square: 'https://lookcos.cn/usr/uploads/2022/04/2067928922.png'
|
||||
alt: 'cover'
|
||||
tags: ["源码研究", "标准库", "golang", "gin"]
|
||||
theme: 'light'
|
||||
featured: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
服务器在收到请求时,首先进入路由 Router,接着路由会根据 request 请求的路径,找到对应的处理器(Handler),处理器再根据 request 进行处理并构造 response 进行返回。
|
||||
|
||||
## 利用标准库实现一个简单HTTP Server
|
||||
|
||||
向**main.go**文件写入如下内容:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 方法一
|
||||
type HelloContext struct {
|
||||
content string
|
||||
}
|
||||
|
||||
func(h *HelloContext) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, h.content)
|
||||
}
|
||||
|
||||
// 方法二
|
||||
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello, net/http! v2\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.Handle("/v1", &HelloContext{content: "Hello, net/http! v1\n"})
|
||||
http.HandleFunc("/v2", helloHandler)
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
```
|
||||
|
||||
运行后,可以用 curl 工具进行测试:
|
||||
|
||||
```bash
|
||||
mac:~ $ curl 127.0.0.1:8080/v1
|
||||
Hello, net/http! v1
|
||||
mac:~ $ curl 127.0.0.1:8080/v2
|
||||
Hello, net/http! v2
|
||||
```
|
||||
|
||||
这段代码我们用 http.Handle 和 http.HandleFunc 两种方法分别在路径 /v1 和 /v2 上注册了两个 http.Handler。注意:Handle 和 Handler 是两个东西。
|
||||
这两个 Handler 都对 request 进行了处理,并且通过 fmt.Fprintf 方法写入并返回数据。
|
||||
|
||||
## 处理器
|
||||
|
||||
### http.Handler
|
||||
|
||||
先来了解一下 http.Handler (处理器),
|
||||
|
||||
```go
|
||||
type Handler interface {
|
||||
ServeHTTP(ResponseWriter, *Request)
|
||||
}
|
||||
```
|
||||
|
||||
它被定义为一个拥有 ServeHTTP 方法的接口,也就是说任何类型,只要实现了 ServeHTTP 方法,就实现了 http.Handler 接口。
|
||||
|
||||
ServeHTTP 方法会读取 *Request 信息,并且向 ResponseWriter 写入 header 与 body 内容。
|
||||
|
||||
## 路由注册
|
||||
|
||||
### http.Handle
|
||||
|
||||
从 main函数出发,来看 http.Handle 函数源码:
|
||||
|
||||
```go
|
||||
func Handle(pattern string, handler Handler) {
|
||||
DefaultServeMux.Handle(pattern, handler)
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,http.Handle 函数调用了 DefaultServeMux.Handle 方法。
|
||||
|
||||
### http.HandleFunc
|
||||
|
||||
再来看 http.HandleFunc 的源码:
|
||||
|
||||
```go
|
||||
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
|
||||
DefaultServeMux.HandleFunc(pattern, handler)
|
||||
}
|
||||
```
|
||||
|
||||
它也调用了 DefaultServeMux.HandleFunc 方法,再看此方法源码:
|
||||
|
||||
```go
|
||||
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
|
||||
if handler == nil {
|
||||
panic("http: nil handler")
|
||||
}
|
||||
mux.Handle(pattern, HandlerFunc(handler))
|
||||
}
|
||||
```
|
||||
|
||||
不难看出,http.Handle 和 http.HandleFunc 都调用了一个和 ServeMux 对象的 Handle 方法有关。
|
||||
|
||||
这两个方法的作用都是将传入的处理器 (Handler) 注册到对应的路由规则 (pattern)上。
|
||||
|
||||
比如,倒数第三行将 处理器 helloHandler 注册到了路由规则 (路径) /v2 上。这样,当 HTTP 请求的地址是 /v2的时候,就由处理器 helloHandler 来负责处理请求,并且响应。
|
||||
|
||||
mux.Handle 方法中还有一个 http.HandlerFunc ,注意不是 HandleFunc。
|
||||
|
||||
## 适配器与处理器
|
||||
|
||||
### http.HandlerFunc
|
||||
|
||||
```go
|
||||
type HandlerFunc func(ResponseWriter, *Request)
|
||||
```
|
||||
|
||||
HandlerFunc 可以理解为一个适配器,它允许使用普通的函数成为处理器 Handler 对象,前提是这个普通函数拥有 func(ResponseWriter, *Request) 签名。
|
||||
|
||||
上文说到,任何类型只要实现了 ServeHTTP 方法,那它就实现了 Handler接口,它就是一个 Handler 类型。
|
||||
|
||||
```go
|
||||
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
f(w, r)
|
||||
}
|
||||
```
|
||||
|
||||
这里呢,非常的巧妙,HandlerFunc 类型实现了 ServeHTTP 方法,并且又将 ServeHTTP方法的参数传给了自身。
|
||||
|
||||
也就是说:
|
||||
|
||||
1. 一个普通的函数,只要参数是 ResponseWriter 和 *Request,或者换种标准点的说法,它的函数签名为 func(ResponseWriter,*Request),那么它就是 HandlerFunc 类型。
|
||||
2. 由于 HandlerFunc 自身实现了 ServeHTTP方法,所以这个普通函数又实现了 Handler 接口,成了 Handler 类型。
|
||||
|
||||
到这里,如何注册、DefaultServeMux 和 ServeMux 是什么,我们暂时还不知道,为了便于理解,这个下文再说。
|
||||
|
||||
## 监听与服务
|
||||
|
||||
### http.ListenAndServe
|
||||
|
||||
接着往下走,看一下 http.ListenAndServe 做了哪些事情:
|
||||
|
||||
```go
|
||||
// ListenAndServe listens on the TCP network address addr and then calls
|
||||
// Serve with handler to handle requests on incoming connections.
|
||||
// Accepted connections are configured to enable TCP keep-alives.
|
||||
//
|
||||
// The handler is typically nil, in which case the DefaultServeMux is used.
|
||||
//
|
||||
// ListenAndServe always returns a non-nil error.
|
||||
func ListenAndServe(addr string, handler Handler) error {
|
||||
server := &Server{Addr: addr, Handler: handler}
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
```
|
||||
|
||||
不难看出,http.ListenAndServe 负责监听 TCP 网络地址 addr, 代码中写的是`:8080` 也即是监听 8080 端口,并且处理相关的请求。
|
||||
|
||||
这里传入的第二个参数是 Handler 类型,根据注释可以看出:如果传入值为 nil ,那么将会使用 DefaultServeMux 。
|
||||
|
||||
## 服务复用器
|
||||
|
||||
### DefaultServeMux
|
||||
|
||||
```go
|
||||
// DefaultServeMux is the default ServeMux used by Serve.
|
||||
var DefaultServeMux = &defaultServeMux
|
||||
|
||||
var defaultServeMux ServeMux
|
||||
```
|
||||
|
||||
说白了,DefaultServeMux 是 ServeMux 类型的一个实例,由标准库创建。
|
||||
|
||||
下面看 ServeMux 结构体的源码。
|
||||
|
||||
### ServeMux
|
||||
|
||||
ServeMux 是一个结构体,它的作用是服务复用器。
|
||||
|
||||
```go
|
||||
type ServeMux struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]muxEntry
|
||||
es []muxEntry // slice of entries sorted from longest to shortest.
|
||||
hosts bool // whether any patterns contain hostnames
|
||||
}
|
||||
```
|
||||
|
||||
因为涉及并发,所以这里有个读写锁 mu,主要用于保护下面的 map 类型的成员 m。
|
||||
|
||||
es 与 hosts 和路由规则匹配有关。
|
||||
|
||||
这里重点关注一下 m,它是一个 map ,key 是 string 类型的路由表达式,val 是 muxEntry 类型的结构体。
|
||||
|
||||
```go
|
||||
type muxEntry struct {
|
||||
h Handler
|
||||
pattern string
|
||||
}
|
||||
```
|
||||
|
||||
muxEntry 结构体,描述了路由规则 pattern 对应的处理器 h。
|
||||
|
||||
### mux.Handle
|
||||
|
||||
上文中,http.Handle 和 http.HandleFunc 都调用了 mux.Handle 方法。
|
||||
|
||||
它是结构体 ServeMux 的方法,也就是说,此方法主要把 Handler 对象注册到给定的 pattern 上,也即**路由注册**。
|
||||
|
||||
```go
|
||||
// Handle registers the handler for the given pattern.
|
||||
// If a handler already exists for pattern, Handle panics.
|
||||
func (mux *ServeMux) Handle(pattern string, handler Handler) {
|
||||
// 为了保护 ServeMux 成员 map 类型的 m 的读写,分别在方法开始和结束的时候进行加锁和解锁的操作。
|
||||
mux.mu.Lock()
|
||||
defer mux.mu.Unlock()
|
||||
// 如果路由规则 pattern 为空,则直接 panic。
|
||||
if pattern == "" {
|
||||
panic("http: invalid pattern")
|
||||
}
|
||||
// 如果 http.Handler 类型的处理器 handler 为空,则panic。
|
||||
if handler == nil {
|
||||
panic("http: nil handler")
|
||||
}
|
||||
// 如果路由规则 pattern 已经存在,则直接 panic。
|
||||
if _, exist := mux.m[pattern]; exist {
|
||||
panic("http: multiple registrations for " + pattern)
|
||||
}
|
||||
// 如果成员 m 为空,则 make 一个新的 map。
|
||||
if mux.m == nil {
|
||||
mux.m = make(map[string]muxEntry)
|
||||
}
|
||||
// 创建一个 muxEntry,并将 pattern 对应的 Handler 放进去。
|
||||
e := muxEntry{h: handler, pattern: pattern}
|
||||
// 写入 m, key 为 pattern ,value 为新建的 muxEntry 类型的 e ,也即新增一个路由规则。
|
||||
mux.m[pattern] = e
|
||||
// 如果路由规则以字符 / 结尾,则给将新建的 muxEntry 类型的 e 放到成员 es 中。
|
||||
// es 是一个切片,使用 http.appendSorted 方法加入元素,以确保 es 中的元素(路由)是从最长到最短。
|
||||
if pattern[len(pattern)-1] == '/' {
|
||||
mux.es = appendSorted(mux.es, e)
|
||||
}
|
||||
// 最后,如果路由规则不是以字符 / 开头,那么给成员 hosts 赋值 true 。
|
||||
if pattern[0] != '/' {
|
||||
mux.hosts = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### mux.ServeHTTP
|
||||
|
||||
我把 ServeMux 的 ServeHTTP 方法简称为 mux.ServeHTTP,下文也是一样。
|
||||
|
||||
```go
|
||||
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
if r.RequestURI == "*" {
|
||||
if r.ProtoAtLeast(1, 1) {
|
||||
w.Header().Set("Connection", "close")
|
||||
}
|
||||
w.WriteHeader(StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h, _ := mux.Handler(r)
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
```
|
||||
|
||||
ServeMux 结构体同样实现了 ServeHTTP 方法,也即它也实现了 Handler 接口,是一个 Handler 类型的对象。
|
||||
|
||||
但它并不负责处理具体的请求,篇幅有限,这里给出,调用的 mux.Handler 方法签名:
|
||||
|
||||
```go
|
||||
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string)
|
||||
```
|
||||
|
||||
总的来说,mux.ServeHTTP 调用了 mux.Handler 方法,通过 host 和 path 找到具体的 处理器 Handler 和路由规则 pattern ,然后让对应的 Handler 的 ServeHTTP 方法去处理请求。
|
||||
|
||||
## 连接与请求的处理
|
||||
|
||||
其实搞懂上面方法以及其之间的关系,对于进一步的学习 Go Web 框架 (比如 Gin ) 就已经有很大的帮助了。从监听与服务开始,代码更加底层,这里我主要关心的是,一次请求是如何到达 ServeHTTP 的。
|
||||
|
||||
http.ListenAndServe 方法中,使用传入的监听地址 addr 和处理器 handler 初始化一个 HTTP 服务器 http.Server。
|
||||
|
||||
Server 结构体,主要定义了需要跑一个 HTTP Server 所需要的参数:
|
||||
|
||||
### Server
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
Addr string
|
||||
Handler Handler // handler to invoke, http.DefaultServeMux if nil
|
||||
|
||||
TLSConfig *tls.Config
|
||||
ReadTimeout time.Duration
|
||||
ReadHeaderTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
MaxHeaderBytes int
|
||||
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
|
||||
ConnState func(net.Conn, ConnState)
|
||||
ErrorLog *log.Logger
|
||||
BaseContext func(net.Listener) context.Context
|
||||
ConnContext func(ctx context.Context, c net.Conn) context.Context
|
||||
inShutdown atomicBool // true when server is in shutdown
|
||||
disableKeepAlives int32 // accessed atomically.
|
||||
nextProtoOnce sync.Once // guards setupHTTP2_* init
|
||||
nextProtoErr error // result of http2.ConfigureServer if used
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[*net.Listener]struct{}
|
||||
activeConn map[*conn]struct{}
|
||||
doneChan chan struct{}
|
||||
onShutdown []func()
|
||||
}
|
||||
```
|
||||
|
||||
这些参数不是重点,接着往下。
|
||||
|
||||
### server.ListenAndServe
|
||||
|
||||
```go
|
||||
func (srv *Server) ListenAndServe() error {
|
||||
if srv.shuttingDown() {
|
||||
return ErrServerClosed
|
||||
}
|
||||
addr := srv.Addr
|
||||
if addr == "" {
|
||||
addr = ":http"
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.Serve(ln)
|
||||
}
|
||||
```
|
||||
|
||||
Server 结构体的 ListenAndServe 方法会监听 TCP 网络地址 addr ,然后调用 srv.Serve 处理传入连接的请求。
|
||||
|
||||
### srv.Serve
|
||||
|
||||
```go
|
||||
func (srv *Server) Serve(l net.Listener) error {
|
||||
// ...省略部分
|
||||
for {
|
||||
// 循环监听 TCP 连接
|
||||
rw, err := l.Accept()
|
||||
if err != nil {
|
||||
...省略部分
|
||||
connCtx := ctx
|
||||
if cc := srv.ConnContext; cc != nil {
|
||||
connCtx = cc(connCtx, rw)
|
||||
if connCtx == nil {
|
||||
panic("ConnContext returned nil")
|
||||
}
|
||||
}
|
||||
tempDelay = 0
|
||||
c := srv.newConn(rw)
|
||||
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
|
||||
go c.serve(connCtx)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Serve 方法在 Listenner l 上接受传入的连接,并且为每一个连接创建 goroutine 。这些 gorutines 会读取请求并且调用 srv.Handler 去响应它们。
|
||||
|
||||
### c.Serve
|
||||
|
||||
```go
|
||||
// Serve a new connection.
|
||||
func (c *conn) serve(ctx context.Context) {
|
||||
// ...
|
||||
for {
|
||||
// 循环接受请求,一个连接可以处理多个请求
|
||||
w, err := c.readRequest(ctx)
|
||||
if c.r.remain != c.server.initialReadLimitSize() {
|
||||
// If we read any bytes off the wire, we're active.
|
||||
c.setState(c.rwc, StateActive, runHooks)
|
||||
}
|
||||
// 这行代码是重点
|
||||
serverHandler{c.server}.ServeHTTP(w, w.req)
|
||||
inFlightResponse = nil
|
||||
w.cancelCtx()
|
||||
if c.hijacked() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### serverHandler
|
||||
|
||||
serverHandler 结构体是一个代理,它会代理 server 的 Handler 或 DefaultServeMux 。
|
||||
|
||||
```go
|
||||
type serverHandler struct {
|
||||
srv *Server
|
||||
}
|
||||
|
||||
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
|
||||
// 这个 handler 就是最初 http.ListenAndServe 传入的 Handler 类型的 handler 。
|
||||
handler := sh.srv.Handler
|
||||
// 如果 http.ListenAndServe 第二个参数是 nil,那么使用 DefaultServeMux 。
|
||||
if handler == nil {
|
||||
handler = DefaultServeMux
|
||||
}
|
||||
if req.RequestURI == "*" && req.Method == "OPTIONS" {
|
||||
handler = globalOptionsHandler{}
|
||||
}
|
||||
|
||||
if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
|
||||
var allowQuerySemicolonsInUse int32
|
||||
req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
|
||||
atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
|
||||
}))
|
||||
defer func() {
|
||||
if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
|
||||
sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
|
||||
}
|
||||
}()
|
||||
}
|
||||
// 调用 handler 的 ServeHTTP 方法处理请求。
|
||||
handler.ServeHTTP(rw, req)
|
||||
}
|
||||
```
|
||||
|
||||
到这里,连接中的请求,就交给了 handler.ServeHTTP 也即 mux.ServeHTTP 方法来处理。
|
||||
|
||||
然后 mux.ServeHTTP 方法中,mux.Handler 方法,会根据 request 中的 host 和 path 信息,找到对应的 Handler, 这个 Handler 再处理信息。
|
||||
|
||||
### 总结
|
||||
|
||||
Go net/http 标准库,能让我们轻易地写出一个高性能的 HTTP Server,但肯定不能满足实际业务开发,比如动态路由、中间件、鉴权等这是标准库所不具有的。
|
||||
|
||||
很多重复性的工作和常用的工具与特性要由框架来封装和实现,go 很多高性能框架 比如 Gin 都是直接封装了 net/http,这一点难能可贵,由此可见 Go 标准库的价值。
|
||||
|
||||
所以学习优秀 Go web 框架的前提就是弄清楚 net/http Server 部分的源码,同时,也能方便更好的去使用和优化框架。
|
||||
本文所使用的源码均来自 go 1.18.3,部分方法说明翻译自官方注释。
|
||||
|
||||
如有不当之处,请批评指出。
|
||||
174
src/pages/posts/talk-golang-slice.md
Normal file
174
src/pages/posts/talk-golang-slice.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: '浅谈 Go 1.18.1的切片扩容机制'
|
||||
pubDate: 2022-04-17
|
||||
description: '从Go源码分析切片的扩容机制。'
|
||||
author: 'Austin'
|
||||
cover:
|
||||
url: 'https://lookcos.cn/usr/uploads/2023/02/1277661091.png'
|
||||
square: 'https://lookcos.cn/usr/uploads/2023/02/1277661091.png'
|
||||
alt: 'cover'
|
||||
tags: ["源码研究", "标准库", "golang", "slice"]
|
||||
theme: 'dark'
|
||||
featured: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
Go 1.18.1的源码大小为439Mib
|
||||
|
||||
```bash
|
||||
root@ubuntu:/home/lookcos/go# du -sh
|
||||
439M .
|
||||
```
|
||||
|
||||
用`grep`命令可以在3秒内找到目标代码所在文件以及行数。
|
||||
|
||||
```bash
|
||||
root@ubuntu:/home/lookcos/go# grep -rn "type slice struct" .
|
||||
./src/runtime/slice.go:15:type slice struct {
|
||||
./src/cmd/compile/internal/types/size.go:20:// type slice struct {
|
||||
```
|
||||
|
||||
在`src/runtime/slice.go`的第十五行,可以看到对`slice`的定义:
|
||||
|
||||
```bash
|
||||
15 type slice struct {
|
||||
// 切片底层数组指针
|
||||
16 array unsafe.Pointer
|
||||
// 切片长度
|
||||
17 len int
|
||||
// 切片容量
|
||||
18 cap int
|
||||
19 }
|
||||
```
|
||||
|
||||
### Go slice的扩容:
|
||||
|
||||
```go
|
||||
nums := []int{1, 2}
|
||||
nums = append(nums, 2, 3, 4)
|
||||
```
|
||||
|
||||
对于上面的代码:
|
||||
|
||||
1. `nums`初始化时,cap大小为2。
|
||||
|
||||
2. 在进行`append`操作时,添加了3个元素。
|
||||
|
||||
此时`old.cap = 2`,容量至少为`cap=5`,那么就简单的扩容让`cap=5`了吗?
|
||||
|
||||
|
||||
|
||||
在 `src/runtime/slice.go`的166行处定义了扩容`slice`的函数。
|
||||
|
||||
```bash
|
||||
166 func growslice(et *_type, old slice, cap int) slice {
|
||||
...
|
||||
188 newcap := old.cap
|
||||
189 doublecap := newcap + newcap
|
||||
190 if cap > doublecap {
|
||||
191 newcap = cap
|
||||
192 } else {
|
||||
193 const threshold = 256
|
||||
194 if old.cap < threshold {
|
||||
195 newcap = doublecap
|
||||
196 } else {
|
||||
197 // Check 0 < newcap to detect overflow
|
||||
198 // and prevent an infinite loop.
|
||||
199 for 0 < newcap && newcap < cap {
|
||||
200 // Transition from growing 2x for small slices
|
||||
201 // to growing 1.25x for large slices. This formula
|
||||
202 // gives a smooth-ish transition between the two.
|
||||
203 newcap += (newcap + 3*threshold) / 4
|
||||
204 }
|
||||
205 // Set newcap to the requested cap when
|
||||
206 // the newcap calculation overflowed.
|
||||
207 if newcap <= 0 {
|
||||
208 newcap = cap
|
||||
209 }
|
||||
210 }
|
||||
211 }
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 计算预估容量newcap
|
||||
|
||||
| 变量 | 含义 | 说明 |
|
||||
| --------- | ------------------------ | ------------------------------ |
|
||||
| old.cap | 扩容前切片容量 | |
|
||||
| newcap | 预估容量 | 默认为扩容前切片容量(old.cap) |
|
||||
| cap | 扩容后至少需要的最小容量 | `old.cap` + 本次新增的元素数量 |
|
||||
| doublecap | 扩容前切片的2倍容量 | old.cap * 2 |
|
||||
|
||||
大致规则如下:
|
||||
|
||||

|
||||
|
||||
其中,当扩容前容量 >= 256时,会按照公式进行扩容,
|
||||
|
||||
```go
|
||||
newcap += (newcap + 3*threshold) / 4
|
||||
```
|
||||
|
||||
相较于1.7版本时,固定按照1.25倍的速率扩容,在1.81版本中改为了
|
||||
|
||||
> // Transition from growing 2x for small slices
|
||||
> // to growing 1.25x for large slices. This formula
|
||||
> // gives a smooth-ish transition between the two.
|
||||
|
||||
大概意思为:这个公式,对于容量小的切片,按照2倍的速率扩容和对于容量大的切片,按照1.25倍的速度扩容,为两者提供了平滑的过渡。
|
||||
|
||||
|
||||
|
||||
回到刚才的代码,按照这个规则,old.cap = 2, cap = 2 + 3 = 5,那么由于 cap > old.cap *2 ,所以**预估容量** newcap = cap = 5
|
||||
|
||||
|
||||
|
||||
### 内存对齐,进一步调整newcap
|
||||
|
||||
经过预估,得到了newcap = 5,但这并不是最终结果。
|
||||
|
||||
```go
|
||||
219 switch {
|
||||
220 case et.size == 1:
|
||||
...
|
||||
226 case et.size == goarch.PtrSize:
|
||||
...
|
||||
232 case isPowerOfTwo(et.size):
|
||||
...
|
||||
245 default:
|
||||
246 lenmem = uintptr(old.len) * et.size
|
||||
247 newlenmem = uintptr(cap) * et.size
|
||||
248 capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
|
||||
249 capmem = roundupsize(capmem)
|
||||
250 newcap = int(capmem / et.size)
|
||||
251 }
|
||||
```
|
||||
|
||||
`et`代表元素类型,所以这一步和元素类型有关。
|
||||
|
||||
以整型为例,预估容量 * 元素类型的大小,也即是 5 * 8 = 40 bytes (64位环境下)。
|
||||
|
||||
那么经过roundupsize函数调整,得到结果为 48 bytes,而48 bytes可以装下6个元素,对应调整代码为:
|
||||
|
||||
```go
|
||||
newcap = int(capmem / et.size)
|
||||
```
|
||||
|
||||
所以,最终容量的大小被调整为6。
|
||||
|
||||
|
||||
|
||||
其中`roundupsize`函数位于在`./src/runtime/msize.go`文件中。
|
||||
|
||||
它的作用是:返回mallocgc将分配的内存块的大小。
|
||||
|
||||
也就是,由Go语言的内存管理模块返回给你需要的内存块,通常这些内存块都是预先申请好,并且被分为常用的规格,比如8,16, 32, 48, 64等。
|
||||
|
||||
这里我们需要的内存是40 bytes,所以会分配一个足够用,且最接近的内存块。所以给48bytes,这时,重新调整后的容量 newcap就为6。
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: '毕业第一年:从学生向3D美术师的转变'
|
||||
pubDate: 2035-8-01
|
||||
description: '如何从学校踏入暴雪娱乐、DNEG等游戏和视觉特效巨擘,经历鼓舞人心的三位3D美术师给出了他们的专业建议。'
|
||||
author: '虚幻引擎官网'
|
||||
cover:
|
||||
url: 'https://cdn2.unrealengine.com/student-to-3d-artist-header-1920x1080-73d477520f8e.jpg?resize=1&w=1920'
|
||||
square: 'https://cdn2.unrealengine.com/student-to-3d-artist-header-1920x1080-73d477520f8e.jpg?resize=1&w=1920'
|
||||
alt: 'cover'
|
||||
tags: ["特写", "影视", "教育", "游戏", "3d", "新闻稿"]
|
||||
theme: 'dark'
|
||||
featured: true
|
||||
---
|
||||
|
||||
|
||||
|
||||
我们都知道,无论哪个领域,离开学校进入职场的感觉有多么令人生畏。你具备合适的技能吗?你学到的技术能在你获得的工作中发挥作用吗?这足以让你夜不能寐。
|
||||
|
||||
最近,我们采访了一些3D美术师,他们谈到了离开学校,在游戏开发、视觉特效和沉浸式媒体等行业中找到新工作后,自己是如何度过这一转型时期的。如果你有兴趣了解可转移技能、你可能存在的缺漏以及不同3D岗位的日常工作,这篇文章正好适合你。
|
||||
|
||||
## Kris Yu:暴雪娱乐副环境美术师
|
||||
|
||||
在《暗黑破坏神4》团队中,团队中的新人环境美术师的日常工作是怎样的?按照Kris Yu的说法,通常,她首先要登录系统,完成游戏开发引擎的同步,然后开始处理分配给她的环境任务。对于该团队的美术师来说,时间管理至关重要,按时完成任务是成功的基础。Kris说:“重点不在于‘尽最大努力’或‘创造最好的美术作品’,而是必须遵守时间安排,确保一切顺利。”
|
||||
|
||||
她学到了一个有用的技巧,那就是将与任务相关的所有说明和图片全部列出来。一旦完成,她就会在上面打勾。由于每处环境所需的细节程度各不相同,她或她的团队很容易遗漏一些需要解决的问题,而这种方法能够帮助Kris避免出差错。
|
||||
|
||||
很快,她又了解到,电子游戏行业的工作需要大量沟通和团队协作,在家办公时尤其如此。“在这个行业中,能够大胆直言,表达自我,并倾听他人的意见是至关重要的。”Kris说,“人们已经预料到我会提出问题,犯下错误。我不会因为自己的错误而感到懊悔和不安,我会吸取教训并改正,这将使我成长得更快。”
|
||||
|
||||
为了更清楚地了解每天的任务,Kris会定期通过语音与同事及团队领导交流。她的团队还会上传视频,在每周的回顾会议中互相提供反馈,她说这是一种很好的学习方式,也体现出了同事对游戏开发的热情。
|
||||
|
||||
Kris认为,如果自己在进入新工作之前更深入地学习了设计方面的知识就好了。当她还是一名学生时,她倾向于将其他美术师的概念设计当作起点,在这个基础上创建3D环境。有了那种坚实的基础,Kris只需要做出一些小改动,就能完善整体环境。
|
||||
|
||||
当Kris开始与《暗黑破坏神4》的团队协作处理场景构建任务后,她需要进一步顾及3D空间中的玩法设计,但她认为自己缺乏基础设计方面的经验。“我的个人项目不像可玩的游戏那样错综复杂,在游戏中,可移动摄像机所展示的各个角度都应该完美。”Kris说,“所以我还要继续学习空间设计,我已经在阅读建筑类的书籍了,希望学到更多关于建筑结构的知识。”
|
||||
|
||||
那么,Kris如何一毕业就能在暴雪找到工作?她在2022年的Rookie Awards中获得了游戏开发年度新秀奖,其作品还在2022年虚幻引擎学生作品展中展出过。如果你经常访问虚幻引擎网站,甚至可能已经看过她的作品,只是没有注意到而已!
|
||||
|
||||

|
||||
|
||||
在诺蒙学院时,她创作的废弃工厂环境还被评为“学期最佳”,这帮助她在成为美术师的道路上迈向了一个新台阶。“这件作品的灵感源自我最喜欢的电子游戏《最后生还者》。”Kris说,“顽皮狗的游戏是我来美国学习游戏美术的动因,我想重现他们的风格,通过环境本身讲述故事。”这是她使用虚幻引擎5创作的第一个项目,用到了许多新技术和技能,还做了一些实验。Kris的所有资产都是在Maya中建模的,随后,她使用ZBrush进行雕刻,并通过Substance Painter和Designer应用经过平铺和烘焙的纹理。
|
||||
|
||||

|
||||
|
||||
然而,失败是成功之母。Kris承认,在创作获奖项目之前,她在另一个项目中犯了一些错误。“我的研究工作做得不好,在早期阶段缺乏耐心。”Kris说,“我太急于进入项目,后来才发现我收集的参考资料不足以让我根据概念图完成作品。所以我选择启动新项目,而不是继续完成一个有太多问题需要解决的旧项目。”
|
||||
|
||||
犯了这个错误后,Kris会花费更多时间为她的场景寻找大量参考图片。她甚至还亲自前往了其中一个地点。“我花了大约两个月的时间,专门收集参考资料和设计草图。说实话,时间有点长,但这是我所有四件作品中最精致的项目。”Kris说。
|
||||
|
||||

|
||||
|
||||
|
||||
最后,如果你正在研究实时技术,请竭尽全力将自己和作品展现在公众眼前,这么做是值得的。这意味着你将开启新的大门,获得新的机遇。问问Kris就知道了。
|
||||
|
||||
“在2022年的Rookie Awards中获奖后,我得到了很多出现在公众视野中的机会,这在一定程度上帮助我成为了暴雪娱乐《暗黑破坏神4》团队中的全职副环境美术师。”Kris说,“当我在今年的The Game Awards中看到这款游戏后,我终于感觉自己是一名专业的美术师了。”
|
||||
Kris给出了以下专业提示:
|
||||
在深入细节之前,先关注全局
|
||||
|
||||
“我强烈建议你更加注重对整体环境建立起清晰的认识。我见过很多学生直接就钻入微小的细节,比如在一个道具上花太多时间,但如果整体结构存在问题,那就等于是在浪费时间。”
|
||||
记录项目的过程
|
||||
|
||||
“你未来的雇主很在意这个过程,即使你制作的是一个简单的灰盒模型,或是犯了一个严重的错误导致项目失败,都应该将过程记录下来。它将展现你的思路和发挥创意解决问题的能力。这也是我能够获得面试机会,随后进入暴雪工作的部分原因。”
|
||||
展示你的作品
|
||||
|
||||
“大多数3D美术师都会在ArtStation上发布作品,我强烈建议你订阅专业版账户,这样可以在公众面前增加知名度。ArtStation甚至还为学生提供了订阅折扣。比如说,《暗黑破坏神4》的美术总监在那里看了我的作品集后,就在LinkedIn上联系了我。我也会在LinkedIn上发布作品,因为很多专业美术师和招聘人员都在那里寻找候选人。”
|
||||
|
||||

|
||||
|
||||
## Josh Carstens:DNEG管线技术总监助理
|
||||
|
||||
Josh最初进入DNEG工作时,是在长片动画部门担任管线技术总监助理,为《加菲猫》和Netflix的原创圣诞电影《那年圣诞》等正在制作的节目提供支持。尽管他在学校积累了不少虚拟制片经验,并因此吸引了DNEG的注意,但他还没有机会运用它们。然而他相信,机会迟早会到来。
|
||||
|
||||
他的团队为美术师、制片协调员、节目主管提供3D管线方面的支持,有时也会相互提供支持。支持任务包括帮助美术师解决与渲染、绑定和模型相关的问题,让他们能够通过DNEG多年积累的内部工具套件完成开发任务。这一切都是为了确保长篇动画电影的制作过程尽可能顺利。
|
||||
|
||||
当Josh开始在DNEG工作时,他颇有感触——对许多学生来说,当他们结束学业和实习,找到毕业后的第一份工作时,或许会体会到相似的感受。“我在DNEG的职责与实习期间的工作相反,都是软件和编程方面的,属于传统的3D动画制作,严格说来,这些都不是我在学校时的学习重点。”Josh说,“但既然我已经熟悉了所有基础知识,我可以在此基础上更进一步,熟悉Python和影片制作过程。”
|
||||
|
||||
在入职DNEG之前,Josh在Production Resource Group(PRG)有过一段短暂但富有成效的实习经历,这帮助他熟练地掌握了虚拟制片技术。“在实习中,硬件是我们的主要关注点,而且创作形式自由,现在想来,我仍感觉十分新奇。我从中深入地了解了LED技术,对我接下来的学期大有帮助。”Josh说。
|
||||
图片由Josh Carstens提供
|
||||
对于年轻的3D美术师来说,当进入一个岗位时,他们会希望自己已经掌握了某些特定的知识,这是合情合理的。Josh指出,总体而言,他认为自己如果具备更多关于Git和版本控制系统的知识就好了。“我有过这样的经历,我曾花大量时间为我的工作仓库变更基底,然后它闲置了一个月,这时有更新进来了,于是我必须弄清楚如何将我的代码与更新合并。”Josh说,“如果很多内部仓库都有自己的系统,需要据此决定如何建立分支和贡献代码,你可能会感觉很头疼。”然而,Josh在学习过程中的大部分任务都是使用C++或MATLAB完成的,所以他对编程的熟悉程度有助于他应对目前的工作。
|
||||
|
||||
在罗彻斯特理工学院时,Josh曾导演过影片《Reverie》,他日益丰富的虚拟制片技能再次得到了运用。项目由他主导,因此他能够做出富有创意的决定,然后在虚幻引擎中工作,完成后期制作。“我处理了一些紧急状况,例如,我们的Axis Studio授权加密狗在拍摄前故障了,我需要让支持人员连夜给我们邮寄一个新的,否则我们就完全不能使用动作捕捉了。”Josh说,“能够领导这样一个项目,我感觉很满足。”
|
||||
|
||||

|
||||
|
||||
他还认为,虚拟制片和虚幻引擎是一对最佳搭档。“在Epic Games的推动下,虚幻引擎成了虚拟制片的代名词,而且它的授权条款对于新手也非常友好,如果你的计算机显卡和CPU符合要求,那么它就是你的理想之选。”Josh说。
|
||||
|
||||
虚拟制片新手可以看看虚幻引擎网站上的众多学习资源,它们可以补充你所使用的其他教育资源的缺漏之处。
|
||||
Josh给出了一条建议:
|
||||
“为了在DNEG工作,我从纽约州北部搬到了加拿大的蒙特利尔。在这个过程中,搬到新城市时的社交孤立感是我必须面对的问题,尤其是,我在这里无法说母语。在我以前的生活中,我从来没有离开过家人和朋友,我的生活方式发生了很大的改变,我觉得我没有做好充足的准备。然而,我已经认识到,这种感觉非常正常,我想对其他具有相同处境的年轻专业人士说,你并不孤单。花点时间适应,不要害怕与其他人交往。”DNEG最近开始为员工提供免费的法语课程,Josh希望能够借此机会认识其他部门的新朋友。寻找处境类似的其他人(如别的新员工)是减少孤立感的好方法。
|
||||
|
||||
Lara Rende:德雷塞尔大学学生
|
||||
Lara尚未毕业,但她对资深专家所使用的工具并不感到陌生。事实上,我们对她的故事了解得越多,就越觉得即将毕业的学生能够从中受到鼓舞。
|
||||
|
||||
“不要害怕。”Lara说,“我还记得,我的教授在大一的第一堂课上就打开了Maya,在第二堂课上打开了虚幻引擎4。最初,这会让人感到不知所措,但那时我绝不会想到,到了大四,我会参与这么多项目。”
|
||||
|
||||
她之所以能够积累如此丰富的经验,部分原因是她的教授们都知道,她渴望尝试新出现的沉浸式技术。他们曾指导Lara参与宾夕法尼亚州当地植物园的联合研究项目。Lara不仅能够帮忙重现一座历史悠久的建筑(杜邦故居),还可以在这个过程中同时运用多种技术,包括iOS激光雷达、摄影测量和虚幻引擎5。
|
||||
|
||||

|
||||
|
||||
虽然摄影参考资料是3D建模的重要资源,但对Lara来说,最好的方法还是亲自参观一个地方。她花了一天时间游览长木花园,这不仅有助于她感受这个地方的整体状况,还能直接观察到她的团队要重现的房屋,这份经历最终成了Lara在这个项目中最珍爱的时刻。“那一天,我收获了许多参考照片、视频和个人笔记。”Lara说,“在这座历史悠久的建筑里,我知道我正在做的项目将帮助人们看到这个地方300年前的样子,这让我感觉心潮澎湃。”
|
||||
|
||||
然而,尽管Lara的团队使用激光雷达和摄影测量技术扫描了杜邦故居,但他们选择将它们留作参考,在Maya中为房屋建模,这样可以在虚幻引擎中自由地为它添加纹理并制作动画。
|
||||
|
||||
除了3D建模外,Lara还热衷于学习动作捕捉技术。作为德雷塞尔动作捕捉社团的现任社长,她不仅帮助教导社团成员了解动作捕捉的完整过程,还与朋友们一起完成了独立的动作捕捉实践。有时候,她也会单纯享受捕捉韩流舞蹈动作的乐趣。
|
||||
|
||||
Lara最近参加了导演Ian Fursa独立虚拟制片的拍摄,并测试了这些动作捕捉技能。借此机会,她能够通过绝佳视角了解到摄影棚的拍摄过程。拍摄期间,Lara帮助搭建了摄影棚环境,并根据需要配置了动作捕捉摄像机,以便将Vicon追踪到的数据传入虚幻引擎。她还负责建立和拆解布景,操作动作捕捉系统,设置虚拟制片屏幕,以及标记捕捉对象。当她与Remington Scott合作,为Meta制作Notorious B.I.G.的Sky’s The Limit VR演唱会时,她再次采用了这一流程,并取得了巨大的成功。
|
||||
|
||||

|
||||
|
||||
那么,这名最忙碌的学生现在在做什么呢?她很快就要毕业了。不忙于学习的时候,Lara会腾出时间寻找工作,她也在考虑攻读研究生课程是否是最好的选择。且不管她如何选择,她说:“我知道,无论我毕业后做什么工作,我都会继续学习和探索。”
|
||||
|
||||
显然,当Lara进入职场时,3D建模、摄影测量和动作捕捉等方面的实践经验将对她大有帮助。但她的好奇心和不断学习的动力才是真正值得我们学习的。
|
||||
|
||||

|
||||
|
||||
## Lara给出了以下专业提示:
|
||||
|
||||
“在提供给我的所有项目中,我都会全身心投入。当教授需要我在虚幻引擎中创建一个可玩的VR关卡时,即使我没有任何经验,我还是硬拼着完成了。我参与的所有项目都将为我提供非常宝贵的学习经验。如果你真的想在3D领域做出一些成就,就全身心地投入进来吧。人们可以方便地获取到许多工具和教程。虚幻引擎5是免费的!慢慢来,相信自己。”
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: '虚幻引擎5.1带来电影、广播、动画和实况活动新功能'
|
||||
pubDate: 2025-05-01
|
||||
description: '从通过摄像机内视效实现的虚拟制片,到动画项目以及激情四射的实况广播和活动,虚幻引擎为媒体和娱乐管线提供了更庞大、更健壮、更易用的工具集。来看看有哪些新功能吧。'
|
||||
author: '虚幻引擎官网'
|
||||
cover:
|
||||
url: 'https://cdn2.unrealengine.com/unreal-engine-5-1-media-and-entertainment-header-1920x1080-d314b1b23459.jpg?resize=1&w=1920'
|
||||
square: 'https://cdn2.unrealengine.com/unreal-engine-5-1-media-and-entertainment-header-1920x1080-d314b1b23459.jpg?resize=1&w=1920'
|
||||
alt: 'cover'
|
||||
tags: ["功能", "动画", "广播与实况", "虚幻引擎"]
|
||||
theme: 'light'
|
||||
featured: false
|
||||
---
|
||||
|
||||
|
||||
随着虚幻引擎5.1的发布,它所带来的改进将使所有行业的创作者受益。在这篇博文中,我们将特别关注真人与动画影视内容(包括虚拟制片和LED舞台项目)的创作者、广播行业从业者,以及实况活动制作团队,看看他们将享受到哪些新功能。
|
||||
|
||||
过去两年,媒体和娱乐行业的专业人士以及各种规模的公司对虚幻引擎的采用率呈指数级增长,由此诞生了一些成功的项目,如YouTube上由表演驱动的角色动画节目《Xanadu》、Pixomondo的Caledon足球广告以及Fox Sports的新虚拟演播室。在虚幻引擎5.1中,我们的目标是为行业提供比以往更加庞大、健壮和易用的工具集,使他们能够在过去的成功基础上取得更长远的进步。
|
||||
虚幻引擎5.1为媒体和娱乐行业提供的新功能
|
||||
增强对舞台操作的支持
|
||||
在LED舞台快节奏、高压力环境中工作的舞台操作员将新迎来专门的摄像机内视效编辑器,它支持一系列为操作员需要执行的任务而专门定制的工作流程。这在很大程度上消除了舞台操作员在大纲视图中搜寻特定对象和控件的需要。
|
||||
|
||||
我们计划在未来几周发布与该编辑器功能相对应的iOS应用程序,其用户界面经过调整,专为触摸屏交互而设计。相关新闻已报道,我们改进了远程控制API的用户界面、用户体验和性能,使你能够更加迅速、便捷地构建基于浏览器的强大自定义远程控件。
|
||||
|
||||
新编辑器搭载了一个经过改进的发光板系统界面,可显示nDisplay墙的预览。除了使创建、移动和编辑发光板以及保存预设变得直观和高效外,新的发光板系统还可以在墙上维持发光板的形状,消除失真。
|
||||
|
||||

|
||||
|
||||
## 在虚幻引擎5.1中编辑发光板
|
||||
|
||||
颜色校正对于LED舞台工作流程而言至关重要,它是弥合LED墙与实体布景,并对LED墙内容进行微调的关键手段。在摄像机内视效编辑器中,你还可以访问全新的颜色校正窗口(CCWs),它能够单独地将调整应用至其背后的任何东西(类似于颜色分级应用程序中的Power Window),同时还能够逐个对Actor应用颜色校正,减少了对复杂的遮罩的需求。
|
||||
|
||||
## 改进的媒体板工作流程和EXR回放
|
||||
|
||||
还记得要点击几十次才能将EXR或影片文件添加到关卡或Sequencer轨道中吗?在虚幻引擎5.1中,新的媒体板Actor允许你从内容浏览器中轻松拖放镜头。此外,使用适当的SSD RAID,你现在还可以在引擎中或通过nDisplay回放拥有Mipmap贴图、未经压缩的平铺EXR。为了实现最佳回放效果,我们还添加了将EXR转换为恰当格式的功能。
|
||||
|
||||
## 经过全面改造的虚拟摄像机
|
||||
|
||||
与此同时,虚幻引擎中的虚拟摄像机得到了全面改造,它具备了全新的底层框架,能够利用Epic的像素流送技术提高响应速度和可靠性;用户界面也得到了更新,它拥有以摄像机为中心的现代化设计,摄像机操作员将感到更加熟悉。我们还增加了连接硬件设备的功能,为将来的用户界面定制奠定基础。
|
||||
虚幻引擎5.1中改进的虚拟摄像机
|
||||
|
||||

|
||||
|
||||
## 改进的DMX工作流程
|
||||
|
||||
为了将虚幻引擎更加无缝地集成到庞大的DMX生态系统中,我们增强了对MVR格式的支持,以囊括与灯具、制图和配接相关的功能;当虚幻引擎和光照控制台需要共享DMX数据时,这些数据可以在两个系统之间进行同步。我们还改进了像素映射系统的用户体验,以支持通过虚幻引擎内容数据驱动日益复杂的DMX光照灯具。
|
||||
|
||||
## 光照的增强
|
||||
|
||||
虚幻引擎的全动态全局光照和反射系统Lumen现在提供了对nDisplay的初步支持。有了Lumen,当执行改变太阳角度、调整光源或定位反光板等操作时,间接光照会动态做出适应性调整。之前,这些变更还需要一个烘焙步骤,导致创作流程被打断。
|
||||
|
||||

|
||||
|
||||
## 虚幻引擎5.1中Lumen对nDisplay的支持
|
||||
|
||||
目前,可支持的光源数量并不多(总计大约五到七个光源,取决于显卡)。为了应对需要更多光源的复杂场景,我们还改进了GPU Lightmass,添加了对天空大气、固定天空光照以及光源功能(如IES配置文件和矩形光源纹理)的支持,并全面提升了质量和性能。
|
||||
动画和绑定的改进
|
||||
值得一提的是,虚幻引擎5.1为动画内容(尤其是角色)的制作者改进了内置的动画制作工具集。
|
||||
|
||||
现在处于测试阶段的机器学习(ML)变形器能够使用自定义的Maya插件训练将在虚幻引擎中实时运行的机器学习模型,从而为非线性变形器、复杂的专有绑定或任意变形生成近似的高保真模型。这允许你模拟电影质量级的变形,如弯曲的肌肉、隆起的静脉和拉扯的皮肤。
|
||||
|
||||

|
||||
|
||||
角色变形方面的其他改进包括完善了变形器图形编辑器,从而简化了图形的创建和编辑。
|
||||
|
||||
另一方面,控制绑定朝着完全程序化绑定的方向得到了进一步完善,提高了绑定团队的影响力和扩展能力。其核心框架的更新包括一个新的构造事件,允许你通过图表生成绑定层级,还有一个自定义用户事件,用于创建和触发“将FK对齐至IK”之类的绑定事件。
|
||||
|
||||
通过这些更新,你可以创建一个单独的控制绑定资产,它能够自行构建,适应骨骼比例和属性各不相同的角色。例如,同一个控制绑定经过自行调整,可以适应三根手指的怪物或五根手指的人类,你无需对绑定资产做出任何更改。
|
||||
|
||||
我们还扩展了虚幻引擎的多轨非线性动画编辑器Sequencer,为其增加了对约束的支持,包括位置、旋转和查看点。通过这些,你可以快速、轻松地在任何控制绑定或Actor之间创建关系并制作动画,例如,使摄像机始终跟随一名角色;让一名角色始终手握方向盘;制作小丑玩抛接球的杂耍动画;或约束一名牛仔的臀部,使他在马移动时自然地坐在马鞍上,并且手握缰绳。
|
||||
|
||||
## 虚幻引擎5.1中Sequencer对约束的支持
|
||||
|
||||

|
||||
|
||||
Sequencer还通过蓝图和Python脚本公开了更多功能;我们还为其重构了用户界面和用户体验,这不仅提高了稳定性和可扩展性,也完善了动画创作和编辑的工作流程。
|
||||
除此之外,还有很多……
|
||||
|
||||
这些只是虚幻引擎5.1中面向媒体和娱乐工作流程的部分新功能和改进。请访问版本说明,查看完整的功能列表。
|
||||
29
src/pages/posts/vr-games-week-2023-starts-next-week.md
Normal file
29
src/pages/posts/vr-games-week-2023-starts-next-week.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
layout: '../../layouts/MarkdownPost.astro'
|
||||
title: '2023年VR游戏周即将到来!'
|
||||
pubDate: 2023-04-17
|
||||
description: 'Vr Games Week 2023 Starts Next Week'
|
||||
author: '虚幻引擎官网'
|
||||
cover:
|
||||
url: 'https://cdn2.unrealengine.com/vr-week-2023-header-4-1920x1080-376e6c48383f.jpg?resize=1&w=1920'
|
||||
square: 'https://cdn2.unrealengine.com/vr-week-2023-header-4-1920x1080-376e6c48383f.jpg?resize=1&w=1920'
|
||||
alt: 'cover'
|
||||
tags: ["功能", "动画", "广播与实况", "虚幻引擎"]
|
||||
theme: 'light'
|
||||
featured: false
|
||||
---
|
||||
|
||||
PlayStation VR2即将于下周发布。为表庆祝,我们准备介绍数部即将登陆此平台的虚幻引擎VR游戏,以及一些准备登陆Meta Quest、PCVR等平台的虚幻VR作品。
|
||||
|
||||

|
||||
|
||||
2023年VR游戏周将从2月21日(周二)一直持续到2月25日(周六)。届时,我们将向大家介绍一系列优秀的VR游戏案例,并推介以下项目:
|
||||
《Hubris》
|
||||
《黑相集:之字路VR》
|
||||
《行尸走肉:圣徒与罪人第二章》
|
||||
《穿越火线:塞拉小队》
|
||||
一期以VR为主题的Inside Unreal直播
|
||||
一款尚未公布的VR游戏
|
||||
还有更多精彩内容等你来发现!
|
||||
|
||||
欲了解最新消息,请收藏我们的VR游戏周活动页面.
|
||||
11
src/pages/rss.xml.js
Normal file
11
src/pages/rss.xml.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import rss, { pagesGlobToRssItems } from '@astrojs/rss';
|
||||
|
||||
export async function get() {
|
||||
return rss({
|
||||
title: "Austin's Blog",
|
||||
description: "Site description",
|
||||
site: 'https://astro-blog.qum.cc',
|
||||
items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
|
||||
customData: `<language>zh-cn</language>`,
|
||||
});
|
||||
}
|
||||
32
src/pages/tags/[tag].astro
Normal file
32
src/pages/tags/[tag].astro
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import ArchivePostList from "../../layouts/ArchivePostList.astro";
|
||||
|
||||
const { tag } = Astro.params;
|
||||
const { posts } = Astro.props;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await Astro.glob("../posts/*.md");
|
||||
const uniqueTags = [...new Set(allPosts.map((post) => post.frontmatter.tags).flat())];
|
||||
return uniqueTags.map((tag) => {
|
||||
const filteredPosts = allPosts.filter((post) => post.frontmatter.tags.includes(tag));
|
||||
return {
|
||||
params: { tag },
|
||||
props: { posts: filteredPosts },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<BaseLayout primaryTitle={tag}>
|
||||
<section class="archive">
|
||||
<div class="section-content section-tag">
|
||||
<div class="archive-tag">
|
||||
<h2 class="tag-header">{tag}</h2>
|
||||
<div class="tag-post-list">{
|
||||
posts.length !== 0 ? <ArchivePostList posts={posts} /> : <div class="no-posts">暂无文章</div>}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
7337
src/styles/global.css
Normal file
7337
src/styles/global.css
Normal file
File diff suppressed because it is too large
Load diff
32
src/utils.js
Normal file
32
src/utils.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return year + " 年 " + month + " 月 " + day + " 日";
|
||||
}
|
||||
|
||||
// debounce function
|
||||
export function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function () {
|
||||
let context = this;
|
||||
let args = arguments;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
fn.apply(context, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function formatDateV2(date) {
|
||||
// 创建一个Date对象
|
||||
let d = new Date(date);
|
||||
// 使用toLocaleString方法返回本地时间字符串
|
||||
let localTime = d.toLocaleString("zh-CN", {year: "numeric", month: "2-digit", day: "2-digit"});
|
||||
// 去掉字符串中的斜杠和空格
|
||||
let formattedDate = localTime.replace(/\//g, "-").replace(/\s/g, "");
|
||||
// 返回格式化后的日期
|
||||
return formattedDate;
|
||||
}
|
||||
Loading…
Reference in a new issue