Initial commit

This commit is contained in:
Austin 2023-02-23 19:16:45 +08:00 committed by GitHub
commit 644032463b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 17469 additions and 0 deletions

21
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View 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
View 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
![首页](https://cos.lookcos.cn/blog/static/images/preview/preview_index.png)
### Dark mode
![文章黑暗模式](https://cos.lookcos.cn/blog/static/images/preview/preview_dark.png?)
### Normal article
![普通文章模式](https://cos.lookcos.cn/blog/static/images/preview/preview_light.png)
### Syntax highlighting
![Syntax](https://cos.lookcos.cn/blog/static/images/preview/preview_syntaxHighlighting.png)
### Three display model of images
![](https://cos.lookcos.cn/blog/static/images/preview/preview_different_mode.png)
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
![alt content|wide](a.png)
```
<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
View 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

File diff suppressed because it is too large Load diff

19
package.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

10
public/static/css/github-dark.min.css vendored Normal file
View 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
View 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
View 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}

View 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}

View 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
View 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%}

View 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

File diff suppressed because one or more lines are too long

View 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
View 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);
}
}

View 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}`)} />

View 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">
&copy; {`2018-${year} ${SITE_NAME}`}
<a href="//github.com/austin2035/astro-air-blog">astro-air-blog</a>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,7 @@
---
import Navigation from "./Navigation.astro";
---
<header>
<Navigation />
</header>

View 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
View 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
View file

@ -0,0 +1 @@
/// <reference types="astro/client" />

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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>

View file

@ -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
---
![HomePod 汇集了多项 Apple 创新技术、Siri 智能和智能家居功能,为用户带来前所未有的聆听体验。](https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-hero-230118_big.jpg.large_2x.jpg)
加利福尼亚州库比提诺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 设计优美,外观由无缝透声织网和背光触控板构成。|inline](https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-2-up-230118_big.jpg.large_2x.jpg)
## 强劲的声学表现
HomePod 呈现出类拔萃的音质,低音醇厚深沉,高频惊艳动人。定制研发的高振幅低音单元、振幅高达惊人的 20 毫米的强劲电机驱动振膜、内置低音均衡器麦克风、底部环绕着由 5 个波束成形高音单元组成的阵列共同打造强大的声学体验。S7 芯片结合软件和系统感应技术,提供更加先进的计算音频,彻底发挥声学系统的全部潜能,呈现前所未有的聆听体验。
![高振幅低音单元、强劲电机、内置低音均衡器麦克风、波束成形五高音单元阵列,共同打造强大的声学体验。|inline](https://www.apple.com.cn/newsroom/images/product/homepod/lifestyle/Apple-HomePod-internals-230118_inline.jpg.large_2x.jpg)
利用室内空间感应技术HomePod 可识别附近表面反射的声音判断是否靠近墙壁等表面并对音频进行实时调整。5 个高音单元组成的波束成形阵列利用精确指向控制分离与定向传送直达声和环境声,让用户在清澈的人声和醇美的器乐声中尽情沉醉。
用户可聆听 Apple Music 曲库中的数千万首歌曲1通过单个 HomePod 或用两个 HomePod 组成立体声组合享受空间音频。用户可以通过 Siri 了解各种音乐知识,并根据艺人、歌曲、歌词、年代、类型、心情或活动搜索音乐。
多个 HomePod 带来更出色的体验 两个及更多 HomePod 或 HomePod mini 可解锁一系列强大功能。利用支持多房间音频的隔空播放技术2用户只要说“嘿 Siri”或者触碰并按住 HomePod 顶部,就能用多个 HomePod 播放同一首歌曲,或在不同 HomePod 上播放不同歌曲,甚至使用多个 HomePod 进行广播,传话到各个房间。
用户还可以把两个 HomePod 放在一起组成立体声组合3。立体声组合不但可以分离左右声道还能和谐地播放双声道音频营造比传统立体声扬声器更宽广、更具沉浸感的声场呈现出类拔萃的聆听体验。
![用户可以使用两个 HomePod 组成立体声组合,打造更宽广、更具沉浸感的声场。](https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-stereo-pair-230118_big.jpg.large_2x.jpg)
## 无缝整合 Apple 生态系统
利用超宽带技术,用户可以将 iPhone 上播放的任何声音,如喜爱的歌曲、播客甚至通话直接转移到 HomePod4。房间中的任何人都可以拿着 iPhone 靠近 HomePod播放建议会自动出现在 iPhone 屏幕上从而轻松控制音频播放或获取个性化歌曲与播客推荐。HomePod 还能识别最多 6 个用户的语音,让所有家庭成员都能轻松聆听## 私人歌单、设置提醒和日历事项。
HomePod 的“查找”功能可以让遗落的设备播放声音,帮用户定位 iPhone 等 Apple 设备。用户还能向 Siri 询问好友或家人的位置需要对方在“查找”app 中选择共享自己的位置)。
![房间中的任何人都可以拿着 iPhone 靠近 HomePod播放建议会自动出现在 iPhone 屏幕上,从而轻松控制音频播放或获取个性化歌曲与播客推荐。](https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-ecosystem-230118_big.jpg.large_2x.jpg)
## 智能家居必备
依托声音识别功能5HomePod 可以听到烟雾或一氧化碳警报声,识别出此类声音后,直接向用户的 iPhone 发送通知。全新的内置温度和湿度感应器可衡量室内环境,让用户设置自动化操作,例如房间内到达一定温度时关上窗帘,或者打开风扇。
通过 Siri顾客无需动手就能控制单一设备或者创建场景例如让多个智能家居设备在早上同时开始工作或者设置反复出现的自动化操作例如“嘿 Siri每天早晨日出时打开窗帘6”。当用户通过 Siri 操控暖气等未能以肉眼分辨是否已开关的配件或者操控位于不同房间的配件时HomePod 会发出新的确认音效。经过重制的海洋、森林、雨声等环境音效将进一步整合到体验中,让顾客可以为场景、自动化和警报添加新的声音。
用户也可以在重新设计的家庭 app 中直观地操控、查看和管理配件。家庭 app 提供了“环境”、“灯”和“安全”等新类别,并提供全新多机位视图,让用户轻松设置和控制智能家居。
![用户可以在重新设计的家庭 app 中直观地操控、查看和管理配件。|inline](https://www.apple.com.cn/newsroom/images/product/homepod/standard/Apple-HomePod-smart-home-230118_big.jpg.large_2x.jpg)
## 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 设备均使用最新版本软件。智能家居配件需单独购买。

View file

@ -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
---
![无边记为 iPhone、iPad 和 Mac 提供了灵活多用的画布,在同一个地方汇聚对话主题、内容及灵感创意。](https://www.apple.com.cn/newsroom/images/product/apps/standard/Apple-Freeform-hero_big.jpg.large_2x.jpg)
无边记是一款全新 app今日正式推出包括 iOS、iPadOS 和 macOS 最新版本。无边记帮助用户在灵活多用的画布上管理并以视觉方式展示内容,在同一个地方查看、共享与协作,而不必考虑排版或页面大小问题。用户无需离开画板,即可添加不同类型的文件并实时预览。无边记为协作而设计,让用户更轻松地邀请他人在同一块白板上工作还能在 FaceTime 通话期间与他人协作。无边记白板存储在 iCloud 上,用户可以在不同设备间同步内容。
“无边记为 iPhone、iPad 和 Mac 用户进行视觉化协作开启了无限可能。”Apple 全球产品营销副总裁 Bob Borchers 表示,“无边记带来了无边无际的画布,支持上传多种不同类型的文件,集成了 iCloud还提供了诸多协作功能为用户随时随地创建头脑风暴的共享空间。”
## 为创意设计的画布与易用工具
无边记为用户提供了绝佳的白板体验,在同一个地方汇聚灵感创意。无边无际的画布会随着内容添加到白板上而不断扩展,让用户在处理很多文件或与他人协作时获得无限灵活性。通过内置手势支持,用户可以在白板各处流畅转移。
这款 app 提供了多种画笔类型和颜色选项供用户描绘创意、添加注释、绘制图表。iPhone 与 iPad 用户可以在画布上的任意地方用手指作画;无边记还支持 Apple Pencil让用户更轻松地随时随地在 iPad 上描绘灵感。
![无论使用随手写记笔记、用绘画工具画图、用蜡笔或填充工具填色,无边记用户都能让创意进入新境界。](https://www.apple.com.cn/newsroom/images/product/apps/standard/Apple-Freeform-Markup_big.jpg.large_2x.jpg)
iPad Pro 上的无边记中展示着随手写笔记和一系列绘画工具。
无论使用随手写记笔记、用绘画工具画图、用蜡笔或填充工具填色,无边记用户都能让创意进入新境界。
无边记支持大量文件类型包括照片、视频、音频、文档、PDF、网站链接和地图地点链接、便签、图形、图表等等。用户还可以利用 iPhone 和 iPad 摄像头直接在白板中插入图片或扫描文档。无边记还提供了全面的图形素材库,包括 700 多种可选图形,用户可以改变这些图形的颜色和大小、添加文本,甚至创建个性化图形。
用户可以将内容从文件和访达等 app 拖放至画板,并通过内置对齐指导轻松保持画板整洁有序。用户只需轻点两下,不必离开画板即可通过速览预览内容,还能同时播放多个视频文件,创建动态视图。图片和 PDF 等内容可以在画板上锁定位置,协作者可以在对象上方或周围添加注释,这意味着无边记极为适合用来为建筑平面图或家装计划描绘灵感,或者供教练在篮球战术板上布置战术。
iPad Pro 的无边记画板支持多种类型的文件。
无边记画板支持多种类型的文件,用户无需离开画板即可实时预览,特别适合为学校项目汇集文档或者规划下一次旅行。
![无边记画板支持多种类型的文件,用户无需离开画板即可实时预览,特别适合为学校项目汇集文档或者规划下一次旅行。](https://www.apple.com.cn/newsroom/images/product/apps/standard/Apple-Freeform-files-support_big.jpg.large_2x.jpg)
## 协作空间
无论用户在办公室或是出门在外,无论是独立工作还是与他人协作,无边记都能派上大用场。无边记支持多人在同一块白板上协作,为集体项目或与好友规划度假方案创造共享创意空间。
利用信息 app 的全新协作功能,用户只需将无边记画板拖入信息对话串就能邀请他人在白板上展开协作。对话串的所有成员都会自动被邀请加入画板,马上开始协作。有人编辑内容后,活动更新会出现在信息对话串顶部。
这款 app 内置了 FaceTime 通话功能,用户使用无边记时,轻点屏幕右上角的协作按钮,即可发起 FaceTime 通话。依托快速同步功能和 iCloud 集成,所有协作者都可以查看其他人添加的内容或者改动。无边记白板会在 iPhone、iPad 和 Mac 间同步,用户可以通过链接或电子邮件邀请他人,还能以 PDF 格式导出画板,或者截取屏幕。
iPad Pro 上,无边记正在使用 FaceTime 通话协作功能。
依托 FaceTime 和 iCloud 集成,无边记为协作而设计,帮助用户更轻松地邀请他人在画板上共同工作。
![依托 FaceTime 和 iCloud 集成,无边记为协作而设计,帮助用户更轻松地邀请他人在画板上共同工作。](https://www.apple.com.cn/newsroom/images/product/apps/standard/Apple-Freeform-FaceTime_big.jpg.large_2x.jpg)
## 推出时间
今日起,所有支持 iOS 16.2、iPad OS 16.2 或 macOS Ventura 13.1 的 iPhone、iPad 及 Mac 均可免费下载无边记。

View 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
View 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
---
![在印度马哈拉施特拉邦Apple 与 Applied Environmental Research Foundation 展开合作,对红树林进行保护与培育。这一海岸森林生态系统可以吸收和存储大气中的二氧化碳。|wide](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Alibaug-canoe_Full-Bleed-Image.jpg.large_2x.jpg)
在马哈拉施特拉邦繁华的滨海城市孟买以南仅约 96 公里的地方,出现了一个截然不同的世界。繁华都市的摩天大厦、餐厅、酒店、购物区、不计其数的“嘟嘟车”与现代汽车逐渐消失,未铺装的道路、棕榈树、山羊、拉车的牛、小型露天市场和路边餐馆出现在视野里。
Raigad 县的 Alibaug 连接了孟买与通向阿拉伯海的河网。海岸地区长有 21000 公顷红树林。红树林是地球最为天然的守护者之一,能够抵御气候变化带来的种种影响,包括突如其来的暴雨、海潮上升、热带气旋或飓风,甚至海啸。同时,红树林还能起到碳汇的作用,吸收大气中的二氧化碳,并将其存储在土壤、植物和其他沉积物中,这一机制被称为“蓝碳”。
Applied Environmental Research FoundationAERF在 2021 年获得了 Apple 的资助。该组织正在这一地区进行探索,计划通过在当地社区中创设可持续的替代行业,来培育红树林生态系统的生物多样性与适应能力并从中受益,从而保护红树林的未来。保护协议将向当地村民提供持续性支持,以换取对土地的保护,并促进当地经济转型,使之有赖于保持红树林的完好与健康。
![在印度马哈拉施特拉邦 Raigad 县,当地村庄的渔民以捕捞红树林内及海岸的小虾、泥蟹等水产为生。](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-fisherman-with-net_big_carousel.jpg.large_2x.jpg)
AERF 还将从保护国际基金会Conservation International2018 年启动的哥伦比亚 Cispatá 试行蓝碳项目获得的经验应用到印度的红树林。
“对抗气候变化是在为全球各地受这场危机影响最严重的社区而奋斗。从哥伦比亚到肯尼亚再到菲律宾人们的生活和生计饱受气候变化的威胁这也正是我们工作的重点。”Apple 环境、政策与社会事务副总裁 Lisa Jackson 表示,“我们在印度的新的合作项目延续了这一努力,帮助当地社区从红树林的保育中收获经济利益,并抵御气候变化的恶劣影响。”
AERF 主席 Archana Godbole 从小就热爱大自然。“植物代表了年龄与时间,”她表示,“而树木则代表了耐心。它们是时间的无声观众——我越是研究树木、了解树木,就越在它们面前感到渺小。我的经历让我逐渐意识到,我想为保护和拯救树木与森林而工作。”
![Applied Environmental Research Foundation 主席 Archana Godbole 是一位植物分类学家,她在过去三十年间一直致力于以社区为基础的环保工作。](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Archana-Godbole_big.jpg.large_2x.jpg)
专业植物分类学家 Godbole 在过去三十年间一直致力于以社区为基础的环保工作。在 Raigad 县AERF 正在寻求与当地社区达成环保协议。由于海水侵袭和人造堤坝的损毁,当地居民的作物与农田的损失惨重。
“这里世代居住的人们本是农民但突然之间海洋来到了他们的门口。”Godbole 说道,“人们学会了新的技巧,来应对新的局面。现在我们已经知道红树林在应对气候变化与碳封存方面作用非常重要。我们很高兴能来到这里,努力与当地居民合作,让红树林为他们带来更多福祉。我们满怀信心地期待,他们能够在心中建立与土地和红树林的深厚联系。”
下面,我们可以看到这些村庄的面貌,以及被很多人称之为家的社区面对日益严峻的环境灾害所表现出的适应能力。
![Karanjveera 村的长者、75 岁的 Namdev Waitaram More 是传统捕鱼技术的行家。他正在帮助 AERF 联系社区成员,讨论红树林保育的机遇,以提供替代行业,支持本地人的生计。|inline](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Namdev-Waitaram-More_inline.jpg.large_2x.jpg)
Karanjveera 是一个小型内陆村庄也是许多农民和渔民及其家人的家园。当地渔民通常以捕捞小虾蟹为主。Namdev Waitaram More 是村里的长者也是传统捕鱼技术的行家。75 岁高龄的 More 一生都与红树林和谐共处,并深深尊敬它们抵御海水侵袭稻田的保护能力。
More 正与表弟帮助其他社区成员与 AERF 联系,探讨村中盐沼和红树林保育问题。“红树林就像海绵。”他说道,“人们与这里的红树林关系密切。如果红树林没了,我们的堤坝就没了,我们的稻田也就没了。因为我们通过食物、堤坝和红树林相连,我们才得以生存。”
![农民 Usha Thakur 来自 Hashiware 村,这里的农田已经完全被淹没。她是首批与 AERF 签订保护协议的村民之一,保护现已覆盖本地的红树林。](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Usha-Thakur_big.jpg.large_2x.jpg)
Usha 和儿子 Tushar Thakur 都是 Hashiware 的农民。这个村庄位于 Amba 河畔,自从本地堤坝于 1990 年溃决后这里的农田就一直淹没在海水之下。这里现已被红树林覆盖但往日的残留依稀可见遭弃置的房屋矗立在泥水中距离河岸仅有几米。Thakur 是首批与 AERF 签订红树林保护协议的村民之一。
从 1996 年开始,曾经属于 Hashiware 村农民的农田就已被红树林覆盖。
“通过我们的工作与对红树林重要性的警示,以及创造可持续创收活动的机会,我们为 Raigad 县的沿海社区带来了希望。”AERF 的 Godbole 介绍道。
![相对陆生森林,健康的红树林生态系统可以在土地中存储更多碳,这种机制被称为“蓝碳”。](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-coastline_big.jpg.large_2x.jpg)
红树林保护印度沿海村庄的能力在近年得到了展现。2004 年,印尼海底强烈地震引发的多重海啸对印度东海岸造成了冲击。从那时起,人们意识到红树林是本地社区的无声守护者,吸收着巨浪的震撼,保护着岸边的村庄。在过去几年间,强热带气旋越来越频繁地侵袭这一地区,包括 2020 年的 Nisarga 和 2021 年的 Tauktae。在 Raigad 县,村民们正在努力保护红树林,从而保护自己的生计与福祉。
![渔民 Mangesh Patil 来自 Ganesh Patti 村。在当地一处堤坝由于海潮上升溃决后,这里已经完全被海水淹没。但他仍然返回了儿时的家。“这是我们出生的地方。”他说道,“我们在这里很开心。所以我们总会回到这里。”](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Mangesh-Patil_big.jpg.large_2x.jpg)
在 Ganesh Patti 村,农民们同意各人负责一部分堤坝的维护,将农田与红树林和河岸分离。但个人的维护工作还不够。当地渔民 Mangesh Patil 的家现已毁坏,被红树林所围绕。海浪和越来越高的海潮逐渐毁掉了他的房子。
但在这个已消失的村落的村民看来,一切似乎发生在一夜之间。
“一天晚上,大家都睡了,夜潮来了,突然之间海水侵入,我们的床铺都湿透了。第二天早晨,我们意识到,整个村子都被淹了。”
潮水退去后居民们清点了土地和生计的损失——他们明白自己必须从零开始了。迁移到邻近的村庄后Patil 和他的兄弟等许多人都还是决定继续返回自己的老家,回到自己的印度教寺庙,在曾是儿时家园的水中捕鱼捞蟹。
“无论大自然给人类什么样的条件我们都应该学会生存。”Patil 说道,“我们就是这样做的。现在,我们与这些红树林之间已经建立了联系。这里是我们出生的地方,我们在这里很开心。所以我们总是会回到这里。”
![Apple 对 AERF 拨款的一部分用于向当地村民提供便携生物质炉具,作为替代烹饪方式,无需再使用砍伐红树林获取的木材来生火。](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-bio-stove_big.jpg.large_2x.jpg)
在与当地村庄签署环保协议提供资金支持的同时Apple 的拨款还将用于购买与分发便携生物质炉具,让人们不再需要砍伐红树林,也能生火做饭。
![来自 Pen Vashi 的渔民 Bhavik Patil 从小就在红树林里玩耍。现在,他靠在泥水中捕鱼捞蟹为生。](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Bhavik-Patil_big.jpg.large_2x.jpg)
来自 Pen Vashi 的渔民 Bhavik Patil 十分熟悉与红树林相关的生计,他帮助 AERF 在 Raigad 县的村庄中与村民探讨相关问题。出生在渔业家庭的 Patil 记得自己小时候,父母会在下河工作前,在红树上绑上秋千,让他和兄弟玩耍。现在,在捕鱼捞蟹之余,他也和很多人一起,与 Mothe Bhal 和 Vithalwadi 等村庄的村民就保护和可持续利用红树林的问题展开磋商。为了保护红树林,他和同伴请求村民收集从树木上自然掉落的干燥树枝。
![Applied Environmental Research Foundation 的联合创始人 Jayant Sarnaik 相信 Raigad 县近年来的气候变化已经让当地村民们认识到保护与他们共存的红树林的重要意义。|inline](https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Jayant-Sarnaik_inline.jpg.large_2x.jpg)
对 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 项目提供支持。该项目旨在促进社区对环境变化的适应能力与可持续生计。

View file

@ -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
---
![图片来自虚幻引擎官网|wide](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-header-1920x1080-2e96869442d6.jpg?resize=1&w=1920)
十二月初当《堡垒之夜大逃杀》第四章第一赛季发布时玩家很难不注意到视觉保真度和细节得到了质的提升。这并不是巧合通过虚幻引擎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间接光照在这类环境中的质量。
![堡垒之夜](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-lumen-1920x1080-81dc92ab2770.jpg?resize=1&w=1920)
## Nanite
Nanite彻底改变了美术师和设计师构建虚拟世界的方式它在突破几何细节极限的同时也简化了创作过程。美术师不再需要考虑网格体LOD或给定对象的正确多边形数量即可实现最佳视觉质量和性能。
在UE 5.0中Nanite出色地实现了不透明的刚性几何体《堡垒之夜》的城市环境充分利用了这一点。无论镜头拉得多近经过建模的砖块、木材和窗户装饰都能展示复杂的细节。
从UE 5.1开始Nanite支持了全局位置偏移和遮罩材质《堡垒之夜》的美术师能够在极精致的动画树木和树叶上使用这项技术。树叶和草叶被建模成了随风自然摇曳的几何体。为了确保在任意渲染距离都能保留所有重要的茂盛细节我们还引入了一种让Nanite简化植被几何体的新方法。
![游戏画面|wide](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-nanite-1920x1080-e363f12e106c.jpg?resize=1&w=1920)
## 虚拟阴影贴图
虚拟阴影贴图VSM与Nanite相结合将美术师在《堡垒之夜大逃杀》第四章中添加的所有复杂几何细节凸显了出来。与传统的级联阴影贴图相比VSM最大的优势在于无论物体是近在玩家眼前还是远在地平线上它都能提供接近像素级精度的阴影细节。
新的《堡垒之夜》几何体为我们带来了一个难题我们需要进一步改进VSM使其与植被及当日时间动态系统相配合——对于我们之前为提高性能而采用的许多阴影缓存方案来说此二者成了一道阻碍。当然这一切都必须符合60FPS的预算。
《堡垒之夜》现在也在室内使用了许多较小的局部光源如电灯或天花板顶灯。VSM缓存在这些地方表现出色。像这样大量使用局部光源让我们有机会在UE 5.1中进一步提高VMS的性能。我们可以通过“单通道投射”选项单次批量化处理许多光源的阴影更新从而提高GPU的利用率。我们还建立了启发式方法将远距离光源的阴影更新分摊到多个帧中。
![游戏画面测试](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-virtual-shadow-maps-1920x1080-b6c0aa573202.jpg?resize=1&w=1920)
## 局部曝光
Lumen允许美术师创造出对比度极高的场景例如在非常暗的房间中透过窗户看到明亮的室外或者在同一视图中房屋表面被明亮的天空和太阳照亮而室内几乎漆黑一片。对于这种视图单一的曝光值效果不佳明亮区域最终会出现过度曝光而黑暗区域则会近乎全黑无法看清。
UE5中的曝光系统针对《堡垒之夜大逃杀》第四章做出了改进提供了更多工具让美术师实现其所需的美术方向同时也带来了更出色的性能。美术师现在可以分别控制局部曝光在高光和阴影中的应用这有助于在高动态范围光照条件下保留细节。除此之外局部曝光与后期处理管线中的其他部分尤其是泛光和镜头光斑更加深入地整合到了一起使结果更加一致。
我们还新支持了在忽略材质属性的同时,基于照度计算自动曝光。这有助于材质在不同的光照条件下保持外观的一致,并提高游戏过程中的图像稳定性。
## 时序超级分辨率
时序超级分辨率TSR通过将一些成本高昂的渲染计算分摊到多个帧中使我们能够大幅削减渲染漂亮4K图像的成本。考虑到《堡垒之夜大逃杀》第四章60FPS的目标我们需要对TSR做出许多性能和质量上的优化。因此UE 5.1中的TSR比我们在UE 5.0中发布的版本更好也更快。我们还改进了TSR依据配置文件低级、中级、高级和史诗级对性能和质量的调整范围。
我们会在即将发布的一篇博客文章中深入探讨这些改进的技术细节。
## 云层
为了配合《堡垒之夜大逃杀》第四章中视觉保真度的飞跃我们改进了虚幻引擎5.1中云层渲染的质量。为了满足严格的性能预算UE5在内部会以较低的分辨率渲染云层并对其进行向上采样。以前这会导致遮挡了背景云层的前景网格体周围产生不完整的边缘致使明亮的天空颜色在边缘处渗透出来。在UE 5.1中,我们可以更好地解决全分辨率下云层向上采样的问题,不会出现这种渗透。我们还改进了当网格体从云层前穿行而过时,对云层的时序重建工作,有效地减少了因移除遮挡而产生的尾迹。
![云层图片](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-4-chapter-clouds-1920x1080-fded45421b6c.jpg?resize=1&w=1920)
## Niagara
《堡垒之夜》利用Niagara视效系统创造了由美术师主导的程序化破坏效果。Niagara能够对游戏做出反应每当武器击中目标时都能生成并准确模拟飞溅的碎片。
《堡垒之夜大逃杀》第四章中新推出的高保真火焰使用基于体素的传播机制更好地表现了火焰在表面和地形上的蔓延同时还支持在载具及物理对象间传播火焰。为了正确放置火焰并将其对准到燃烧表面该系统使用了在引擎内部经过烘焙的Niagara流体模拟并将其应用到了Niagara粒子中数据读取自火焰游戏系统
![游戏画面](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-high-fidelity-fire-1920x1080-27061074e8c8.jpg?resize=1&w=1920)
## 对世界构建的支持
随着第四章的发布《堡垒之夜大逃杀》现在在每个平台上都使用了UE5最新的世界构建和自动流送解决方案。其中包括世界分区、数据层、关卡实例、自动HLOD以及一Actor一文件。这套工具集有助于建立更具协作性的工作流程并提升关卡设计师和美术师构建世界的效率。
与《堡垒之夜》之前版本中使用的自定义流送解决方案相比目前方案最大的一个不同之处是新的世界分区系统实现了自动化开发者需要付出的时间更少了同时HLOD的过渡也更加稳定提供了更好的整体游戏体验。
第四章包含超过10万个Actor文件为了更有效地处理如此之多的文件我们改进了变更列表窗口增加了新的用户体验和新的非受控变更列表类型并将它与场景大纲视图和主视口更好地集成在了一起。
为了给第四章提供支持,我们做的进一步改进是在寻路网格体(静态和动态)的生成功能中添加对世界分区的支持。这意味着你可以加载一个世界单元,为其生成寻路网格体,将其序列化,然后卸载该单元并继续处理下一单元。
在UE 5.1中,所有开发者都可以使用这些经过改进的功能,并且,随着《堡垒之夜》团队不断对这些功能展开实战测试,我们可以期待在未来版本中看到进一步的改进。
![游戏画面,这张截图将会以宽屏的方式展示|wide](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-world-partition-1920x1080-9d3a0d7ff88c.jpg?resize=1&w=1920)
## 智能对象和状态树
智能对象允许AI在关卡中选择一个对象或区域并动态注入行为。相关的数据和配置如动画会被存储在对象中而不是代理中这实现了大量代理之间的共享促成了更高效的内存利用和更出色的性能无需编辑核心行为即可更灵活地扩展默认行为。
状态树是一种可扩展的通用状态机,它在灵活的决策树结构(以及直观而紧凑的用户界面)中结合了经典的状态机机制,同时保留了高性能。
这两项技术在UE 5.0版本中是作为实验功能发布的但经过《堡垒之夜大逃杀》第四章中的成功部署后它们在5.1版本中已进入生产就绪状态。在第四章之前智能对象只被用于让AI播放简单的动画或表情。通过在智能对象流程中整合状态树我们成功地实现了更复杂、更丰富的机器人行为例如机器人与《堡垒之夜》篝火互动的方式。
![游戏画面](https://cdn2.unrealengine.com/unreal-engine-5-1-features-for-fortnite-chapter-4-smart-objects-1920x1080-844796c16826.jpg?resize=1&w=1920)
## 更高的开发者效率
我们一直在寻找提高开发者效率的方法让他们可以专注于创作过程。《堡垒之夜》采用了新的图形技术如磁盘空间占用量更大的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做出的所有改进,并发现它们有助于你将游戏的视觉保真度提升到新水平。我们期待看到结果!

View file

@ -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 4800 万像素主摄于福州拍摄。|wide](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)
通过 iPhone 14 Pro MaxiPhone 14 ProiPhone 14 以及 iPhone 14 Plus 上突破性创新的摄像头系统,中国的摄影师们拍摄并分享令人惊艳的照片和视频,以此激励更多 iPhone 用户在即将到来的节日假期中捕捉亮丽的城市景观、迷人的自然风光,并记录下与亲人相聚的难忘时刻。
![小北 使用 iPhone 14 Pro 超广角摄像头于福州拍摄。 |inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-with-the-Ultra-wide-camera-by-Xiaobei-Fuzhou_12192022_inline.jpg.large_2x.jpg)
![Hojisan 使用 iPhone 14 Plus 主摄于重庆拍摄。](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Plus-with-the-Main-camera-by-Hojisan-Chongqing_121920221_inline.jpg.large_2x.jpg)
iPhone 14 和 iPhone 14 Plus 为摄影和摄像设立了新标准,搭载具备更大感光元件和像素尺寸的 1200 万像素主摄、首次配备自动对焦功能并采用 ƒ/1.9 光圈的全新前置原深感摄像头,以及超广角摄像头用于拍摄更宽广的场景。除此之外,光像引擎也带来低光表现的巨大跃升。
通过软硬件深度集成,光像引擎能够提升所有摄像头在中低光环境下的照片表现:超广角摄像头表现提升可达 2 倍,原深感摄像头表现提升可达 2 倍,全新主摄表现提升可达 2.5 倍。iPhone 14 系列还推出全新的运动模式——即使在大幅的抖动和运动中,也能轻松拍摄无比丝滑的视频。
![Boogie93 使用 iPhone 14 主摄于上海拍摄。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-with-the-Main-camera-by-Boogie93-Shanghai_12192022_inline.jpg.large_2x.jpg)
![Hojisan 使用 iPhone 14 Plus 主摄于重庆拍摄。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Plus-with-the-Main-camera-by-Hojisan-Chongqing_12222022_inline.jpg.large_2x.jpg)
![赵华鹏 使用 iPhone 14 Plus 主摄于三亚拍摄。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Plus-with-the-Main-camera-by-Jamie-Sanya_12192022_inline.jpg.large_2x.jpg)
iPhone 14 Pro 和 iPhone 14 Pro Max 上搭载的 Pro 级摄像头系统挑战 iPhone 影像的能力极限。iPhone 14 Pro 首次采用搭载四合一像素传感器的全新 4800 万像素主摄,同时配备光像引擎使细节的丰富程度达到前所未有的高度。
![赵华鹏 使用 iPhone 14 Pro Max 主摄于三亚拍摄。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-Max-with-the-Main-camera-by-Jamie-Sanya_12192022_inline.jpg.large_2x.jpg)
![Hojisan 使用 iPhone 14 Pro 主摄于重庆拍摄。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-with-the-Main-camera-by-Hojisan-Chongqing_12192022_inline.jpg.large_2x.jpg)
从日常到专业使用iPhone 14 Pro 上推出的摄像头系统将为每一位用户赋能,令他们的照片和视频更出彩。全新 4800 万像素主摄采用四合一像素传感器,可根据拍摄照片进行调整,还具备第二代传感器位移式光学图像防抖功能,全面提升图像质量。这有利于复杂光线环境下的拍摄,尤其在低光条件下也能拍摄到优质的影像。此外,用户现可以用 4800 万像素拍摄 ProRAW 影像,利用每一个像素,为专业用户实现全新的创意工作流。
![Derrick Zhang 使用 iPhone 14 Pro 4800 万像素主摄于上海拍摄 ProRAW 照片。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-in-48MP-ProRAW-by-Jamie-Shanghai_12192022_big.jpg.large_2x.jpg)
![小北 使用 iPhone 14 Pro 4800 万像素主摄于福州拍摄 ProRAW 照片。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-Max-in-48MP-ProRAW-by-Xiaobei-Fuzhou_12192022_inline.jpg.large_2x.jpg)
加上超广角和长焦摄像头一起iPhone 14 Pro 现在支持 0.5 倍、1 倍、2 倍和 3 倍共计四个变焦选项 ,使用户无需牺牲画面质量,即可在拍摄照片和视频时拥有更多取景选项。丰富的焦距范围可以轻松应对多种场景,足之所至,即成佳作。四合一像素传感器还支持 2 倍长焦选项,利用该传感器正中的 1200 万像素拍摄全分辨率照片和 4K 视频,无需数码变焦。该选项能够以常用焦距带来光学品质成像,对于人像模式等功能来说非常实用。
![赵华鹏 使用 iPhone 14 Pro Max 四个变焦选项于三亚拍摄。](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_05x_12192022_big.jpg.large_2x.jpg)
强大的 A16 仿生芯片和配备 5 核图形处理器的 A15 仿生芯片,是驱动强大功能的幕后功臣,如运动模式让摄像师轻松实现更加稳定、丝滑的视频画面,电影效果模式带来沉浸式叙事风格。
全新的运动模式可拍摄出无比丝滑的视频,应对大幅的抖动、位移和震动,甚至在运动中拍摄时也不例外,让用户在手持拍摄时无需携带云台等外设。该模式支持高达 2.8K 60 fps 视频,而增强的电影效果模式现已支持 4K 分辨率和 24 fps 的电影帧率录制,让用户能够通过生动的叙事释放无穷的创造力,并与全世界分享他们通过 iPhone 所看到的内容。
自适应原彩闪光灯经过全面重新设计,采用九粒 LED 灯珠阵列,会根据用户选择的照片焦距更改闪光模式,使拍摄对象始终处于最佳光线中。与上一代相比,全新闪光灯亮度提升可达 2 倍,光线均匀度提升可达 3 倍。
此外,全新原深感摄像头配备更快的光圈,让低光条件下照片视频更美丽细致,并支持自动对焦功能。
![小北 使用 iPhone 14 Pro 自适应原彩闪光灯于福州拍摄。](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-with-the-Adaptive-True-Tone-flash-by-Xiaobei-Fuzhou_12192022_big.jpg.large_2x.jpg)
![Derrick Zhang 使用 iPhone 14 Plus 前置原深感摄像头于上海拍摄。|inline](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Plus-with-the-front-TrueDepth-camera-by-Derrick-Shanghai_12192022_inline.jpg.large_2x.jpg)
iPhone 14 系列的所有摄像头均支持夜间模式,这意味着当用户在深夜拍摄风景或在低光条件下自拍时,照片会被神奇地点亮。同时,夜间模式现将广角摄像头的想象力提升到全新水平,得益于更大的感光元件和更快的光圈,使 iPhone 14 和 iPhone 14 Plus 上的曝光时间最快提升可达 2 倍。
![赵华鹏 使用 iPhone 14 Pro Max 主摄的夜间模式于三亚拍摄。](https://www.apple.com.cn/newsroom/cn/images/product/iphone/lifestyle/Apple_Shot-on-iPhone-14-models_Shot-on-iPhone-14-Pro-Max-with-the-Main-camera-using-Night-mode-by-Jamie-Sanya_12192022_big.jpg.large_2x.jpg)

453
src/pages/posts/golang.md Normal file
View 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
---
![Go HTTP Server的大致处理流程|wide](https://lookcos.cn/usr/uploads/2023/02/3697706570.png)
服务器在收到请求时,首先进入路由 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,部分方法说明翻译自官方注释。
如有不当之处,请批评指出。

View 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
---
![golang 吉祥物](https://lookcos.cn/usr/uploads/2023/02/1277661091.png)
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 |
大致规则如下:
![https://lookcos.cn/usr/uploads/2022/04/573066505.png](https://lookcos.cn/usr/uploads/2022/04/573066505.png)
其中,当扩容前容量 >= 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语言的内存管理模块返回给你需要的内存块通常这些内存块都是预先申请好并且被分为常用的规格比如816 32 48 64等。
这里我们需要的内存是40 bytes所以会分配一个足够用且最接近的内存块。所以给48bytes这时重新调整后的容量 newcap就为6。

View file

@ -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 Yu提供](https://cdn2.unrealengine.com/student-to-3d-artist-kris-1-1920x1080-edbf5490e1b7.jpg?resize=1&w=1920)
在诺蒙学院时她创作的废弃工厂环境还被评为“学期最佳”这帮助她在成为美术师的道路上迈向了一个新台阶。“这件作品的灵感源自我最喜欢的电子游戏《最后生还者》。”Kris说“顽皮狗的游戏是我来美国学习游戏美术的动因我想重现他们的风格通过环境本身讲述故事。”这是她使用虚幻引擎5创作的第一个项目用到了许多新技术和技能还做了一些实验。Kris的所有资产都是在Maya中建模的随后她使用ZBrush进行雕刻并通过Substance Painter和Designer应用经过平铺和烘焙的纹理。
![图片由Kris Yu提供](https://cdn2.unrealengine.com/student-to-3d-artist-kris-4-1920x1080-f944dc364420.jpg?resize=1&w=1920)
然而失败是成功之母。Kris承认在创作获奖项目之前她在另一个项目中犯了一些错误。“我的研究工作做得不好在早期阶段缺乏耐心。”Kris说“我太急于进入项目后来才发现我收集的参考资料不足以让我根据概念图完成作品。所以我选择启动新项目而不是继续完成一个有太多问题需要解决的旧项目。”
犯了这个错误后Kris会花费更多时间为她的场景寻找大量参考图片。她甚至还亲自前往了其中一个地点。“我花了大约两个月的时间专门收集参考资料和设计草图。说实话时间有点长但这是我所有四件作品中最精致的项目。”Kris说。
![图片由Kris Yu提供](https://cdn2.unrealengine.com/student-to-3d-artist-kris-9-1920x1080-30b604ffff3f.jpg?resize=1&w=1200)
最后如果你正在研究实时技术请竭尽全力将自己和作品展现在公众眼前这么做是值得的。这意味着你将开启新的大门获得新的机遇。问问Kris就知道了。
“在2022年的Rookie Awards中获奖后我得到了很多出现在公众视野中的机会这在一定程度上帮助我成为了暴雪娱乐《暗黑破坏神4》团队中的全职副环境美术师。”Kris说“当我在今年的The Game Awards中看到这款游戏后我终于感觉自己是一名专业的美术师了。”
Kris给出了以下专业提示
在深入细节之前,先关注全局
“我强烈建议你更加注重对整体环境建立起清晰的认识。我见过很多学生直接就钻入微小的细节,比如在一个道具上花太多时间,但如果整体结构存在问题,那就等于是在浪费时间。”
记录项目的过程
“你未来的雇主很在意这个过程,即使你制作的是一个简单的灰盒模型,或是犯了一个严重的错误导致项目失败,都应该将过程记录下来。它将展现你的思路和发挥创意解决问题的能力。这也是我能够获得面试机会,随后进入暴雪工作的部分原因。”
展示你的作品
“大多数3D美术师都会在ArtStation上发布作品我强烈建议你订阅专业版账户这样可以在公众面前增加知名度。ArtStation甚至还为学生提供了订阅折扣。比如说《暗黑破坏神4》的美术总监在那里看了我的作品集后就在LinkedIn上联系了我。我也会在LinkedIn上发布作品因为很多专业美术师和招聘人员都在那里寻找候选人。”
![这是一段很长的文字,用来添加图片描述](https://cdn2.unrealengine.com/student-to-3d-artist-kris-10-1920x1080-dbdd2eb1622e.jpg?resize=1&w=1920)
## Josh CarstensDNEG管线技术总监助理
Josh最初进入DNEG工作时是在长片动画部门担任管线技术总监助理为《加菲猫》和Netflix的原创圣诞电影《那年圣诞》等正在制作的节目提供支持。尽管他在学校积累了不少虚拟制片经验并因此吸引了DNEG的注意但他还没有机会运用它们。然而他相信机会迟早会到来。
他的团队为美术师、制片协调员、节目主管提供3D管线方面的支持有时也会相互提供支持。支持任务包括帮助美术师解决与渲染、绑定和模型相关的问题让他们能够通过DNEG多年积累的内部工具套件完成开发任务。这一切都是为了确保长篇动画电影的制作过程尽可能顺利。
当Josh开始在DNEG工作时他颇有感触——对许多学生来说当他们结束学业和实习找到毕业后的第一份工作时或许会体会到相似的感受。“我在DNEG的职责与实习期间的工作相反都是软件和编程方面的属于传统的3D动画制作严格说来这些都不是我在学校时的学习重点。”Josh说“但既然我已经熟悉了所有基础知识我可以在此基础上更进一步熟悉Python和影片制作过程。”
在入职DNEG之前Josh在Production Resource GroupPRG有过一段短暂但富有成效的实习经历这帮助他熟练地掌握了虚拟制片技术。“在实习中硬件是我们的主要关注点而且创作形式自由现在想来我仍感觉十分新奇。我从中深入地了解了LED技术对我接下来的学期大有帮助。”Josh说。
图片由Josh Carstens提供
对于年轻的3D美术师来说当进入一个岗位时他们会希望自己已经掌握了某些特定的知识这是合情合理的。Josh指出总体而言他认为自己如果具备更多关于Git和版本控制系统的知识就好了。“我有过这样的经历我曾花大量时间为我的工作仓库变更基底然后它闲置了一个月这时有更新进来了于是我必须弄清楚如何将我的代码与更新合并。”Josh说“如果很多内部仓库都有自己的系统需要据此决定如何建立分支和贡献代码你可能会感觉很头疼。”然而Josh在学习过程中的大部分任务都是使用C++或MATLAB完成的所以他对编程的熟悉程度有助于他应对目前的工作。
在罗彻斯特理工学院时Josh曾导演过影片《Reverie》他日益丰富的虚拟制片技能再次得到了运用。项目由他主导因此他能够做出富有创意的决定然后在虚幻引擎中工作完成后期制作。“我处理了一些紧急状况例如我们的Axis Studio授权加密狗在拍摄前故障了我需要让支持人员连夜给我们邮寄一个新的否则我们就完全不能使用动作捕捉了。”Josh说“能够领导这样一个项目我感觉很满足。”
![图片由Josh Carstens提供](https://cdn2.unrealengine.com/student-to-3d-artist-josh-1-1920x1080-8e9c77913e3a.jpg?resize=1&w=1920)
他还认为虚拟制片和虚幻引擎是一对最佳搭档。“在Epic Games的推动下虚幻引擎成了虚拟制片的代名词而且它的授权条款对于新手也非常友好如果你的计算机显卡和CPU符合要求那么它就是你的理想之选。”Josh说。
虚拟制片新手可以看看虚幻引擎网站上的众多学习资源,它们可以补充你所使用的其他教育资源的缺漏之处。
Josh给出了一条建议
“为了在DNEG工作我从纽约州北部搬到了加拿大的蒙特利尔。在这个过程中搬到新城市时的社交孤立感是我必须面对的问题尤其是我在这里无法说母语。在我以前的生活中我从来没有离开过家人和朋友我的生活方式发生了很大的改变我觉得我没有做好充足的准备。然而我已经认识到这种感觉非常正常我想对其他具有相同处境的年轻专业人士说你并不孤单。花点时间适应不要害怕与其他人交往。”DNEG最近开始为员工提供免费的法语课程Josh希望能够借此机会认识其他部门的新朋友。寻找处境类似的其他人如别的新员工是减少孤立感的好方法。
Lara Rende德雷塞尔大学学生
Lara尚未毕业但她对资深专家所使用的工具并不感到陌生。事实上我们对她的故事了解得越多就越觉得即将毕业的学生能够从中受到鼓舞。
“不要害怕。”Lara说“我还记得我的教授在大一的第一堂课上就打开了Maya在第二堂课上打开了虚幻引擎4。最初这会让人感到不知所措但那时我绝不会想到到了大四我会参与这么多项目。”
她之所以能够积累如此丰富的经验部分原因是她的教授们都知道她渴望尝试新出现的沉浸式技术。他们曾指导Lara参与宾夕法尼亚州当地植物园的联合研究项目。Lara不仅能够帮忙重现一座历史悠久的建筑杜邦故居还可以在这个过程中同时运用多种技术包括iOS激光雷达、摄影测量和虚幻引擎5。
![图片由Lara Rende提供](https://cdn2.unrealengine.com/student-to-3d-artist-lara-3-1920x1080-8a6460ca2b0c.jpg?resize=1&w=1920)
虽然摄影参考资料是3D建模的重要资源但对Lara来说最好的方法还是亲自参观一个地方。她花了一天时间游览长木花园这不仅有助于她感受这个地方的整体状况还能直接观察到她的团队要重现的房屋这份经历最终成了Lara在这个项目中最珍爱的时刻。“那一天我收获了许多参考照片、视频和个人笔记。”Lara说“在这座历史悠久的建筑里我知道我正在做的项目将帮助人们看到这个地方300年前的样子这让我感觉心潮澎湃。”
然而尽管Lara的团队使用激光雷达和摄影测量技术扫描了杜邦故居但他们选择将它们留作参考在Maya中为房屋建模这样可以在虚幻引擎中自由地为它添加纹理并制作动画。
除了3D建模外Lara还热衷于学习动作捕捉技术。作为德雷塞尔动作捕捉社团的现任社长她不仅帮助教导社团成员了解动作捕捉的完整过程还与朋友们一起完成了独立的动作捕捉实践。有时候她也会单纯享受捕捉韩流舞蹈动作的乐趣。
Lara最近参加了导演Ian Fursa独立虚拟制片的拍摄并测试了这些动作捕捉技能。借此机会她能够通过绝佳视角了解到摄影棚的拍摄过程。拍摄期间Lara帮助搭建了摄影棚环境并根据需要配置了动作捕捉摄像机以便将Vicon追踪到的数据传入虚幻引擎。她还负责建立和拆解布景操作动作捕捉系统设置虚拟制片屏幕以及标记捕捉对象。当她与Remington Scott合作为Meta制作Notorious B.I.G.的Skys The Limit VR演唱会时她再次采用了这一流程并取得了巨大的成功。
![动态展示](https://cdn2.unrealengine.com/mocap-shoot-fall-00205c43b83c.gif?resize=1&w=1920)
那么这名最忙碌的学生现在在做什么呢她很快就要毕业了。不忙于学习的时候Lara会腾出时间寻找工作她也在考虑攻读研究生课程是否是最好的选择。且不管她如何选择她说“我知道无论我毕业后做什么工作我都会继续学习和探索。”
显然当Lara进入职场时3D建模、摄影测量和动作捕捉等方面的实践经验将对她大有帮助。但她的好奇心和不断学习的动力才是真正值得我们学习的。
![图片由Lara Rende提供](https://cdn2.unrealengine.com/student-to-3d-artist-lara-1-1920x1080-f4917c6683f7.jpg?resize=1&w=1920)
## Lara给出了以下专业提示
“在提供给我的所有项目中我都会全身心投入。当教授需要我在虚幻引擎中创建一个可玩的VR关卡时即使我没有任何经验我还是硬拼着完成了。我参与的所有项目都将为我提供非常宝贵的学习经验。如果你真的想在3D领域做出一些成就就全身心地投入进来吧。人们可以方便地获取到许多工具和教程。虚幻引擎5是免费的慢慢来相信自己。”

View file

@ -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墙的预览。除了使创建、移动和编辑发光板以及保存预设变得直观和高效外新的发光板系统还可以在墙上维持发光板的形状消除失真。
![展示图片](https://cdn2.unrealengine.com/editing-light-cards-in-unreal-engine-5-1-1920x1080-4a117d589f77.jpg?resize=1&w=1920)
## 在虚幻引擎5.1中编辑发光板
颜色校正对于LED舞台工作流程而言至关重要它是弥合LED墙与实体布景并对LED墙内容进行微调的关键手段。在摄像机内视效编辑器中你还可以访问全新的颜色校正窗口CCWs它能够单独地将调整应用至其背后的任何东西类似于颜色分级应用程序中的Power Window同时还能够逐个对Actor应用颜色校正减少了对复杂的遮罩的需求。
## 改进的媒体板工作流程和EXR回放
还记得要点击几十次才能将EXR或影片文件添加到关卡或Sequencer轨道中吗在虚幻引擎5.1中新的媒体板Actor允许你从内容浏览器中轻松拖放镜头。此外使用适当的SSD RAID你现在还可以在引擎中或通过nDisplay回放拥有Mipmap贴图、未经压缩的平铺EXR。为了实现最佳回放效果我们还添加了将EXR转换为恰当格式的功能。
## 经过全面改造的虚拟摄像机
与此同时虚幻引擎中的虚拟摄像机得到了全面改造它具备了全新的底层框架能够利用Epic的像素流送技术提高响应速度和可靠性用户界面也得到了更新它拥有以摄像机为中心的现代化设计摄像机操作员将感到更加熟悉。我们还增加了连接硬件设备的功能为将来的用户界面定制奠定基础。
虚幻引擎5.1中改进的虚拟摄像机
![这是一张宽屏展示图片|wide](https://cdn2.unrealengine.com/enhanced-vcam-in-unreal-engine-5-1-1920x1080-0534e1a4c66f.jpg?resize=1&w=1920)
## 改进的DMX工作流程
为了将虚幻引擎更加无缝地集成到庞大的DMX生态系统中我们增强了对MVR格式的支持以囊括与灯具、制图和配接相关的功能当虚幻引擎和光照控制台需要共享DMX数据时这些数据可以在两个系统之间进行同步。我们还改进了像素映射系统的用户体验以支持通过虚幻引擎内容数据驱动日益复杂的DMX光照灯具。
## 光照的增强
虚幻引擎的全动态全局光照和反射系统Lumen现在提供了对nDisplay的初步支持。有了Lumen当执行改变太阳角度、调整光源或定位反光板等操作时间接光照会动态做出适应性调整。之前这些变更还需要一个烘焙步骤导致创作流程被打断。
![展示图片](https://cdn2.unrealengine.com/lumen-support-for-ndisplay-in-unreal-engine-5-1-1920x1080-33c908592bd0.jpg?resize=1&w=1920)
## 虚幻引擎5.1中Lumen对nDisplay的支持
目前可支持的光源数量并不多总计大约五到七个光源取决于显卡。为了应对需要更多光源的复杂场景我们还改进了GPU Lightmass添加了对天空大气、固定天空光照以及光源功能如IES配置文件和矩形光源纹理的支持并全面提升了质量和性能。
动画和绑定的改进
值得一提的是虚幻引擎5.1为动画内容(尤其是角色)的制作者改进了内置的动画制作工具集。
现在处于测试阶段的机器学习ML变形器能够使用自定义的Maya插件训练将在虚幻引擎中实时运行的机器学习模型从而为非线性变形器、复杂的专有绑定或任意变形生成近似的高保真模型。这允许你模拟电影质量级的变形如弯曲的肌肉、隆起的静脉和拉扯的皮肤。
![机器学习变形器vs. 线性蒙皮(右)|wide](https://cdn2.unrealengine.com/machine-learning-deformer-in-unreal-engine-5-1-off-1920x1080-326c44f4c4a3.jpg?resize=1&w=1000)
角色变形方面的其他改进包括完善了变形器图形编辑器,从而简化了图形的创建和编辑。
另一方面控制绑定朝着完全程序化绑定的方向得到了进一步完善提高了绑定团队的影响力和扩展能力。其核心框架的更新包括一个新的构造事件允许你通过图表生成绑定层级还有一个自定义用户事件用于创建和触发“将FK对齐至IK”之类的绑定事件。
通过这些更新,你可以创建一个单独的控制绑定资产,它能够自行构建,适应骨骼比例和属性各不相同的角色。例如,同一个控制绑定经过自行调整,可以适应三根手指的怪物或五根手指的人类,你无需对绑定资产做出任何更改。
我们还扩展了虚幻引擎的多轨非线性动画编辑器Sequencer为其增加了对约束的支持包括位置、旋转和查看点。通过这些你可以快速、轻松地在任何控制绑定或Actor之间创建关系并制作动画例如使摄像机始终跟随一名角色让一名角色始终手握方向盘制作小丑玩抛接球的杂耍动画或约束一名牛仔的臀部使他在马移动时自然地坐在马鞍上并且手握缰绳。
## 虚幻引擎5.1中Sequencer对约束的支持
![展示](https://cdn2.unrealengine.com/sequencer-support-for-constraints-in-unreal-engine-5-1-1920x1080-126851c2a874.jpg?resize=1&w=1920)
Sequencer还通过蓝图和Python脚本公开了更多功能我们还为其重构了用户界面和用户体验这不仅提高了稳定性和可扩展性也完善了动画创作和编辑的工作流程。
除此之外,还有很多……
这些只是虚幻引擎5.1中面向媒体和娱乐工作流程的部分新功能和改进。请访问版本说明,查看完整的功能列表。

View 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作品。
![PS](https://cdn2.unrealengine.com/vr-week-2023-header-4-1920x1080-376e6c48383f.jpg?resize=1&w=1920)
2023年VR游戏周将从2月21日周二一直持续到2月25日周六。届时我们将向大家介绍一系列优秀的VR游戏案例并推介以下项目
《Hubris》
《黑相集之字路VR》
《行尸走肉:圣徒与罪人第二章》
《穿越火线:塞拉小队》
一期以VR为主题的Inside Unreal直播
一款尚未公布的VR游戏
还有更多精彩内容等你来发现!
欲了解最新消息请收藏我们的VR游戏周活动页面.

11
src/pages/rss.xml.js Normal file
View 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>`,
});
}

View 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

File diff suppressed because it is too large Load diff

32
src/utils.js Normal file
View 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;
}