feat(branding): 原创应用图标——深墨夜色+猎人之月+玻璃笔记卡片+H字标(含 .ico/.png/SVG 源与生成脚本)
@@ -0,0 +1,77 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="tile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#1a1e2b"/>
|
||||
<stop offset="0.55" stop-color="#161a26"/>
|
||||
<stop offset="1" stop-color="#10131c"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sheen" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.09"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="moonGlow" cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0" stop-color="#eef2ff" stop-opacity="0.22"/>
|
||||
<stop offset="1" stop-color="#eef2ff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="moon" x1="0.2" y1="0.1" x2="0.8" y2="0.9">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="1" stop-color="#c9d4f2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="card" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.16"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0.07"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hgrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#f2f5ff"/>
|
||||
<stop offset="1" stop-color="#a7b8f0"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feDropShadow dx="0" dy="10" stdDeviation="16" flood-color="#000000" flood-opacity="0.45"/>
|
||||
</filter>
|
||||
<filter id="cardShadow" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="12" flood-color="#000000" flood-opacity="0.5"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 图标底:深墨夜色圆角方块 -->
|
||||
<rect x="0" y="0" width="512" height="512" rx="116" fill="url(#tile)"/>
|
||||
<!-- 顶部玻璃高光 -->
|
||||
<rect x="0" y="0" width="512" height="260" rx="116" fill="url(#sheen)"/>
|
||||
|
||||
<!-- 夜空细星点(狩猎者之夜) -->
|
||||
<g fill="#e7ecff" opacity="0.5">
|
||||
<circle cx="96" cy="120" r="2.6"/><circle cx="150" cy="196" r="1.7"/>
|
||||
<circle cx="420" cy="360" r="2"/><circle cx="356" cy="92" r="1.6"/>
|
||||
<circle cx="88" cy="340" r="1.8"/><circle cx="452" cy="200" r="2.2"/>
|
||||
</g>
|
||||
|
||||
<!-- 猎人之月(月牙 + 柔光) -->
|
||||
<circle cx="404" cy="116" r="62" fill="url(#moonGlow)"/>
|
||||
<g transform="rotate(-18 404 116)">
|
||||
<circle cx="404" cy="116" r="40" fill="url(#moon)"/>
|
||||
<circle cx="418" cy="106" r="36" fill="#1a1e2b"/>
|
||||
</g>
|
||||
|
||||
<!-- 玻璃笔记卡片(居中,微倾) -->
|
||||
<g transform="rotate(-6 256 268)" filter="url(#cardShadow)">
|
||||
<rect x="96" y="150" width="320" height="236" rx="44" fill="url(#card)" stroke="#ffffff" stroke-opacity="0.4" stroke-width="2"/>
|
||||
<!-- 卡片内部顶部细光条 -->
|
||||
<rect x="112" y="164" width="288" height="30" rx="15" fill="#ffffff" opacity="0.10"/>
|
||||
</g>
|
||||
|
||||
<!-- 字标 H(纯几何,保证跨机器一致) -->
|
||||
<g transform="rotate(-6 256 268)">
|
||||
<!-- 左竖 -->
|
||||
<rect x="204" y="210" width="46" height="120" rx="23" fill="url(#hgrad)"/>
|
||||
<!-- 右竖 -->
|
||||
<rect x="286" y="210" width="46" height="120" rx="23" fill="url(#hgrad)"/>
|
||||
<!-- 横杠 -->
|
||||
<rect x="227" y="254" width="82" height="42" rx="21" fill="url(#hgrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- 笔记内容行(卡片下方三条短线) -->
|
||||
<g transform="rotate(-6 256 268)" fill="#ffffff">
|
||||
<rect x="140" y="338" width="150" height="11" rx="5.5" opacity="0.55"/>
|
||||
<rect x="140" y="360" width="110" height="11" rx="5.5" opacity="0.38"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 612 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
@@ -18,6 +18,7 @@
|
||||
],
|
||||
"asar": true,
|
||||
"win": {
|
||||
"icon": "build/icon.ico",
|
||||
"target": [
|
||||
"nsis",
|
||||
"dir"
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import { deflateSync } from 'node:zlib';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import nodePath from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = nodePath.join(__dirname, '..', 'build');
|
||||
const SIZES = [16, 24, 32, 48, 64, 128, 256, 512];
|
||||
|
||||
// ---------- PNG encoder ----------
|
||||
const CRC_TABLE = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
t[n] = c >>> 0;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
function crc32(buf) {
|
||||
let c = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
function pngChunk(type, data) {
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
|
||||
const typeBuf = Buffer.from(type, 'ascii');
|
||||
const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
||||
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
||||
}
|
||||
function encodePNG(w, h, rgba) {
|
||||
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
|
||||
ihdr[8] = 8; ihdr[9] = 6; // bit depth 8, color type 6 (RGBA)
|
||||
ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
||||
const stride = w * 4;
|
||||
const raw = Buffer.alloc(h * (stride + 1));
|
||||
for (let y = 0; y < h; y++) {
|
||||
raw[y * (stride + 1)] = 0; // filter None
|
||||
rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
|
||||
}
|
||||
const idat = pngChunk('IDAT', deflateSync(raw, { level: 9 }));
|
||||
return Buffer.concat([sig, pngChunk('IHDR', ihdr), idat, pngChunk('IEND', Buffer.alloc(0))]);
|
||||
}
|
||||
|
||||
// ---------- ICO encoder ----------
|
||||
function encodeICO(sizes, pngs) {
|
||||
const header = Buffer.alloc(6);
|
||||
header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(sizes.length, 4);
|
||||
const entries = [];
|
||||
let offset = 6 + 16 * sizes.length;
|
||||
for (let i = 0; i < sizes.length; i++) {
|
||||
const s = sizes[i];
|
||||
const e = Buffer.alloc(16);
|
||||
e[0] = s >= 256 ? 0 : s; e[1] = s >= 256 ? 0 : s;
|
||||
e[2] = 0; e[3] = 0;
|
||||
e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6);
|
||||
e.writeUInt32LE(pngs[i].length, 8);
|
||||
e.writeUInt32LE(offset, 12);
|
||||
entries.push(e);
|
||||
offset += pngs[i].length;
|
||||
}
|
||||
return Buffer.concat([header, ...entries, ...pngs]);
|
||||
}
|
||||
|
||||
// ---------- Rasterizer ----------
|
||||
function blend(buf, W, x, y, r, g, b, a) {
|
||||
if (a <= 0.001) return;
|
||||
x = Math.round(x); y = Math.round(y);
|
||||
if (x < 0 || y < 0 || x >= W || y >= W) return;
|
||||
const i = (y * W + x) * 4;
|
||||
const da = buf[i + 3] / 255;
|
||||
const oa = a + da * (1 - a);
|
||||
if (oa <= 0) return;
|
||||
buf[i] = Math.round((r * a + buf[i] * da * (1 - a)) / oa);
|
||||
buf[i + 1] = Math.round((g * a + buf[i + 1] * da * (1 - a)) / oa);
|
||||
buf[i + 2] = Math.round((b * a + buf[i + 2] * da * (1 - a)) / oa);
|
||||
buf[i + 3] = Math.round(oa * 255);
|
||||
}
|
||||
function inRR(px, py, x, y, w, h, r) {
|
||||
if (px < x || px > x + w || py < y || py > y + h) return false;
|
||||
const cx = Math.max(x + r, Math.min(px, x + w - r));
|
||||
const cy = Math.max(y + r, Math.min(py, y + h - r));
|
||||
const dx = px - cx, dy = py - cy;
|
||||
return dx * dx + dy * dy <= r * r;
|
||||
}
|
||||
function inCircle(px, py, cx, cy, r) {
|
||||
const dx = px - cx, dy = py - cy;
|
||||
return dx * dx + dy * dy <= r * r;
|
||||
}
|
||||
function lerp(a, b, t) { return a + (b - a) * t; }
|
||||
function lerpColor(c1, c2, t) { return [lerp(c1[0], c2[0], t), lerp(c1[1], c2[1], t), lerp(c1[2], c2[2], t)]; }
|
||||
|
||||
// linear gradient stops: [[pos, [r,g,b], alpha], ...]
|
||||
function vGradientColor(stops, y, y0, y1) {
|
||||
const t = Math.min(1, Math.max(0, (y - y0) / (y1 - y0)));
|
||||
for (let i = 0; i < stops.length - 1; i++) {
|
||||
const a = stops[i], b = stops[i + 1];
|
||||
if (t >= a[0] && t <= b[0]) {
|
||||
const lt = (t - a[0]) / (b[0] - a[0] || 1);
|
||||
return [lerp(a[1][0], b[1][0], lt), lerp(a[1][1], b[1][1], lt), lerp(a[1][2], b[1][2], lt), lerp(a[2], b[2], lt)];
|
||||
}
|
||||
}
|
||||
const last = stops[stops.length - 1];
|
||||
return [...last[1], last[2]];
|
||||
}
|
||||
|
||||
// Box blur on alpha channel (for soft shadows)
|
||||
function boxBlur(buf, W, radius) {
|
||||
const H = W;
|
||||
const tmp = new Float32Array(W * H);
|
||||
for (let i = 0; i < W * H; i++) tmp[i] = buf[i * 4 + 3] / 255;
|
||||
const src = tmp.slice();
|
||||
const win = radius * 2 + 1;
|
||||
// horizontal
|
||||
for (let y = 0; y < H; y++) {
|
||||
let acc = 0;
|
||||
for (let x = -radius; x <= radius; x++) acc += src[Math.max(0, Math.min(W - 1, x)) + y * W];
|
||||
for (let x = 0; x < W; x++) {
|
||||
tmp[x + y * W] = acc / win;
|
||||
const add = Math.min(W - 1, x + radius + 1), rem = Math.max(0, x - radius);
|
||||
acc += src[add + y * W] - src[rem + y * W];
|
||||
}
|
||||
}
|
||||
const src2 = tmp.slice();
|
||||
for (let x = 0; x < W; x++) {
|
||||
let acc = 0;
|
||||
for (let y = -radius; y <= radius; y++) acc += src2[x + Math.max(0, Math.min(H - 1, y)) * W];
|
||||
for (let y = 0; y < H; y++) {
|
||||
tmp[x + y * W] = acc / win;
|
||||
const add = Math.min(H - 1, y + radius + 1), rem = Math.max(0, y - radius);
|
||||
acc += src2[x + add * W] - src2[x + rem * W];
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < W * H; i++) buf[i * 4 + 3] = Math.round(tmp[i] * 255);
|
||||
}
|
||||
|
||||
// Affine: rotate around pivot
|
||||
function rotPoint(px, py, px_, py_, cosA, sinA) {
|
||||
const dx = px - px_, dy = py - py_;
|
||||
return [px_ + dx * cosA - dy * sinA, py_ + dx * sinA + dy * cosA];
|
||||
}
|
||||
|
||||
function render(size) {
|
||||
const hi = Math.max(256, size * 2);
|
||||
const k = hi / 512;
|
||||
const W = hi;
|
||||
const buf = new Uint8ClampedArray(W * W * 4);
|
||||
// 循环坐标即缓冲区坐标(缓冲区 W=hi,循环 0..hi);k 只用于设计坐标→缓冲区换算
|
||||
const blendAt = (x, y, r, g, b, a) => blend(buf, W, x, y, r, g, b, a);
|
||||
const sA = (ang) => Math.sin(ang), cA = (ang) => Math.cos(ang);
|
||||
|
||||
// ---- Tile base (vertical gradient, rounded 116) ----
|
||||
const tileStops = [[0, [0x1a, 0x1e, 0x2b], 1], [0.55, [0x16, 0x1a, 0x26], 1], [1, [0x10, 0x13, 0x1c], 1]];
|
||||
for (let y = 0; y < hi; y++) {
|
||||
const c = vGradientColor(tileStops, y / k, 0, 512);
|
||||
for (let x = 0; x < hi; x++) {
|
||||
if (inRR(x / k, y / k, 0, 0, 512, 512, 116)) blendAt(x, y, c[0], c[1], c[2], 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Top sheen (rounded top, white 0.09 -> 0) ----
|
||||
for (let y = 0; y < hi; y++) {
|
||||
const t = Math.min(1, Math.max(0, (y / k) / 260));
|
||||
const a = 0.09 * (1 - t);
|
||||
for (let x = 0; x < hi; x++) {
|
||||
if (a > 0.002 && inRR(x / k, y / k, 0, 0, 512, 512, 116)) blendAt(x, y, 255, 255, 255, a);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Stars ----
|
||||
const stars = [[96, 120, 2.6, 0.5], [150, 196, 1.7, 0.5], [420, 360, 2, 0.5], [356, 92, 1.6, 0.5], [88, 340, 1.8, 0.5], [452, 200, 2.2, 0.5]];
|
||||
for (const [sx, sy, sr, sa] of stars) {
|
||||
const r0 = Math.max(0, Math.floor((sy - sr - 1) * k)), r1 = Math.min(hi, Math.ceil((sy + sr + 1) * k));
|
||||
const c0 = Math.max(0, Math.floor((sx - sr - 1) * k)), c1 = Math.min(hi, Math.ceil((sx + sr + 1) * k));
|
||||
for (let y = r0; y < r1; y++) for (let x = c0; x < c1; x++) {
|
||||
if (inCircle(x / k, y / k, sx, sy, sr)) blendAt(x, y, 231, 236, 255, sa);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Moon: glow + crescent ----
|
||||
// glow
|
||||
for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) {
|
||||
const dx = x / k - 404, dy = y / k - 116;
|
||||
const d = Math.sqrt(dx * dx + dy * dy);
|
||||
if (d <= 62) blendAt(x, y, 238, 242, 255, 0.22 * (1 - d / 62));
|
||||
}
|
||||
// crescent (rotate -18 deg around (404,116))
|
||||
const cosA = cA(-18 * Math.PI / 180), sinA = sA(-18 * Math.PI / 180);
|
||||
for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) {
|
||||
const [lx, ly] = rotPoint(x / k, y / k, 404, 116, cosA, sinA);
|
||||
if (inCircle(lx, ly, 404, 116, 40)) {
|
||||
const t = Math.min(1, Math.max(0, (ly - 76) / 80));
|
||||
const mc = lerpColor([255, 255, 255], [201, 212, 242], t);
|
||||
blendAt(x, y, mc[0], mc[1], mc[2], 1);
|
||||
}
|
||||
if (inCircle(lx, ly, 418, 106, 36)) blendAt(x, y, 0x1a, 0x1e, 0x2b, 1);
|
||||
}
|
||||
|
||||
// ---- Card drop shadow (behind card, dark blurred, offset down-right) ----
|
||||
const cardCos = cA(-6 * Math.PI / 180), cardSin = sA(-6 * Math.PI / 180);
|
||||
const cardLocal = (x, y) => rotPoint(x / k, y / k, 256, 268, cardCos, cardSin);
|
||||
const shadowBuf = new Uint8ClampedArray(W * W * 4);
|
||||
for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) {
|
||||
const [lx, ly] = cardLocal(x, y);
|
||||
if (inRR(lx + 4, ly + 12, 96, 150, 320, 236, 44)) {
|
||||
const i = (y * W + x) * 4;
|
||||
shadowBuf[i] = 0; shadowBuf[i + 1] = 0; shadowBuf[i + 2] = 0; shadowBuf[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
boxBlur(shadowBuf, W, Math.max(2, Math.round(18 * k / 4)));
|
||||
for (let i = 0; i < W * W; i++) {
|
||||
const a = shadowBuf[i * 4 + 3] / 255 * 0.5;
|
||||
if (a > 0.002) {
|
||||
const x = i % W, y = (i / W) | 0;
|
||||
blend(buf, W, x, y, 0, 0, 0, a);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Card group (rotate -6 deg around 256,268) ----
|
||||
// draw card (and inner bar + lines) via local coords
|
||||
for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) {
|
||||
const [lx, ly] = cardLocal(x, y);
|
||||
// card body: gradient white 0.16->0.07
|
||||
if (inRR(lx, ly, 96, 150, 320, 236, 44)) {
|
||||
const t = Math.min(1, Math.max(0, (ly - 150) / 236));
|
||||
const a = 0.16 - 0.09 * t;
|
||||
blendAt(x, y, 255, 255, 255, a);
|
||||
}
|
||||
// inner top light bar
|
||||
if (inRR(lx, ly, 112, 164, 288, 30, 15)) blendAt(x, y, 255, 255, 255, 0.10);
|
||||
// card border (ring): draw by testing just outside inner, inside outer
|
||||
// note lines
|
||||
if (inRR(lx, ly, 140, 338, 150, 11, 5.5)) blendAt(x, y, 255, 255, 255, 0.55);
|
||||
if (inRR(lx, ly, 140, 360, 110, 11, 5.5)) blendAt(x, y, 255, 255, 255, 0.38);
|
||||
// H monogram
|
||||
const hStops = [[0, [0xf2, 0xf5, 0xff], 1], [1, [0xa7, 0xb8, 0xf0], 1]];
|
||||
const hc = vGradientColor(hStops, ly, 210, 330);
|
||||
if (inRR(lx, ly, 204, 210, 46, 120, 23) || inRR(lx, ly, 286, 210, 46, 120, 23) || inRR(lx, ly, 227, 254, 82, 42, 21)) {
|
||||
blendAt(x, y, hc[0], hc[1], hc[2], 1);
|
||||
}
|
||||
}
|
||||
// card border: stroke ring (approx) - draw after body
|
||||
for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) {
|
||||
const [lx, ly] = cardLocal(x, y);
|
||||
if (inRR(lx, ly, 96, 150, 320, 236, 44) && !inRR(lx, ly, 100, 154, 312, 228, 40)) {
|
||||
blendAt(x, y, 255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Downsample hi -> size (box average, straight alpha) ----
|
||||
if (size === hi) return Buffer.from(buf);
|
||||
const out = Buffer.alloc(size * size * 4);
|
||||
const ratio = hi / size;
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
let pr = 0, pg = 0, pb = 0, pa = 0;
|
||||
const x0 = Math.floor(x * ratio), y0 = Math.floor(y * ratio);
|
||||
const x1 = Math.min(hi, Math.ceil((x + 1) * ratio)), y1 = Math.min(hi, Math.ceil((y + 1) * ratio));
|
||||
for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) {
|
||||
const i = (yy * W + xx) * 4;
|
||||
const aa = buf[i + 3] / 255;
|
||||
if (aa > 0) { pr += buf[i] * aa; pg += buf[i + 1] * aa; pb += buf[i + 2] * aa; pa += aa; }
|
||||
}
|
||||
const o = (y * size + x) * 4;
|
||||
if (pa > 0) { out[o] = Math.round(pr / pa); out[o + 1] = Math.round(pg / pa); out[o + 2] = Math.round(pb / pa); out[o + 3] = Math.round(pa * 255); }
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- Main ----------
|
||||
async function main() {
|
||||
const pngs = [];
|
||||
for (const s of SIZES) {
|
||||
const rgba = render(s);
|
||||
const png = encodePNG(s, s, rgba);
|
||||
await fs.writeFile(nodePath.join(OUT, 'icon', s + '.png'), png);
|
||||
pngs.push(png);
|
||||
console.log('rendered', s + 'px', png.length, 'bytes');
|
||||
}
|
||||
const ico = encodeICO(SIZES, pngs);
|
||||
await fs.writeFile(nodePath.join(OUT, 'icon.ico'), ico);
|
||||
console.log('icon.ico', ico.length, 'bytes');
|
||||
console.log('DONE');
|
||||
}
|
||||
await main();
|
||||