更换 DOME 风格图标,About 增加基于 OBS 二次开发说明
- 图标重新绘制为 DOME 风格:蓝色半球穹顶 + 白色三叶片光圈 + CAIIC 三色环绕圆弧 (obs.png、obs-studio.ico、updater 用旧版 ico 同步更换,附生成脚本 tools/dome-icon-gen.js) - About 保留原有 logo/名称/版本号,新增 "Based on OBS Studio (GPLv2) / Secondary development by CAIIC" 说明及官网链接 - deploy 同步重新编译的 obs64.exe(版本仍为 1.0.0)
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 39 KiB |
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>520</width>
|
<width>520</width>
|
||||||
<height>300</height>
|
<height>340</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -73,6 +73,22 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="basedOn">
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true">Based on <a href="https://obsproject.com">OBS Studio</a> (GPLv2)<br/>Secondary development by CAIIC</string>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="textInteractionFlags">
|
||||||
|
<set>Qt::TextBrowserInteraction</set>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 21 KiB |
206
tools/dome-icon-gen.js
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// DOME-style icon generator for the CAIIC OBS fork.
|
||||||
|
// Renders a blue observatory/planetarium dome with a white three-blade
|
||||||
|
// aperture (nod to the OBS logo) wrapped by three CAIIC-colored arcs
|
||||||
|
// (orange top, green lower-left, yellow lower-right).
|
||||||
|
// Usage: node dome-icon-gen.js <out-dir>
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
const DEG = Math.PI / 180;
|
||||||
|
|
||||||
|
// ---------- CRC32 / PNG ----------
|
||||||
|
const crcTable = (() => {
|
||||||
|
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 = crcTable[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||||||
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
function chunk(type, data) {
|
||||||
|
const out = Buffer.alloc(8 + data.length + 4);
|
||||||
|
out.writeUInt32BE(data.length, 0);
|
||||||
|
out.write(type, 4, 'ascii');
|
||||||
|
data.copy(out, 8);
|
||||||
|
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function pngEncode(width, height, rgba) {
|
||||||
|
const stride = width * 4;
|
||||||
|
const raw = Buffer.alloc((stride + 1) * height);
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
raw[y * (stride + 1)] = 0;
|
||||||
|
rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
|
||||||
|
}
|
||||||
|
const ihdr = Buffer.alloc(13);
|
||||||
|
ihdr.writeUInt32BE(width, 0);
|
||||||
|
ihdr.writeUInt32BE(height, 4);
|
||||||
|
ihdr[8] = 8; // bit depth
|
||||||
|
ihdr[9] = 6; // RGBA
|
||||||
|
return Buffer.concat([
|
||||||
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||||
|
chunk('IHDR', ihdr),
|
||||||
|
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
||||||
|
chunk('IEND', Buffer.alloc(0)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function icoEncode(images) {
|
||||||
|
// images: [{size, png}]
|
||||||
|
const header = Buffer.alloc(6);
|
||||||
|
header.writeUInt16LE(1, 2);
|
||||||
|
header.writeUInt16LE(images.length, 4);
|
||||||
|
let offset = 6 + images.length * 16;
|
||||||
|
const entries = [];
|
||||||
|
for (const img of images) {
|
||||||
|
const e = Buffer.alloc(16);
|
||||||
|
e[0] = img.size >= 256 ? 0 : img.size;
|
||||||
|
e[1] = img.size >= 256 ? 0 : img.size;
|
||||||
|
e[4] = 1; // planes
|
||||||
|
e.writeUInt16LE(32, 6);
|
||||||
|
e.writeUInt32LE(img.png.length, 8);
|
||||||
|
e.writeUInt32LE(offset, 12);
|
||||||
|
offset += img.png.length;
|
||||||
|
entries.push(e);
|
||||||
|
}
|
||||||
|
return Buffer.concat([header, ...entries, ...images.map((i) => i.png)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- geometry ----------
|
||||||
|
function clamp(x, a, b) { return Math.min(Math.max(x, a), b); }
|
||||||
|
function mix(a, b, t) { return a + (b - a) * t; }
|
||||||
|
function mix3(c1, c2, t) { return [mix(c1[0], c2[0], t), mix(c1[1], c2[1], t), mix(c1[2], c2[2], t)]; }
|
||||||
|
|
||||||
|
// signed distance to a circular arc stroke (rounded caps), angles in radians
|
||||||
|
function arcSDF(px, py, cx, cy, r, a0, a1, w) {
|
||||||
|
const dx = px - cx, dy = py - cy;
|
||||||
|
const d = Math.hypot(dx, dy);
|
||||||
|
let ang = Math.atan2(dy, dx);
|
||||||
|
// normalize ang into [a0, a0 + 2PI)
|
||||||
|
const TWO_PI = Math.PI * 2;
|
||||||
|
while (ang < a0) ang += TWO_PI;
|
||||||
|
while (ang >= a0 + TWO_PI) ang -= TWO_PI;
|
||||||
|
let dist;
|
||||||
|
if (ang <= a1) {
|
||||||
|
dist = Math.abs(d - r);
|
||||||
|
} else {
|
||||||
|
const e0x = cx + r * Math.cos(a0), e0y = cy + r * Math.sin(a0);
|
||||||
|
const e1x = cx + r * Math.cos(a1), e1y = cy + r * Math.sin(a1);
|
||||||
|
dist = Math.min(Math.hypot(px - e0x, py - e0y), Math.hypot(px - e1x, py - e1y));
|
||||||
|
}
|
||||||
|
return dist - w / 2;
|
||||||
|
}
|
||||||
|
// rounded rect sdf
|
||||||
|
function rrSDF(px, py, cx, cy, hw, hh, r) {
|
||||||
|
const qx = Math.abs(px - cx) - (hw - r);
|
||||||
|
const qy = Math.abs(py - cy) - (hh - r);
|
||||||
|
const ax = Math.max(qx, 0), ay = Math.max(qy, 0);
|
||||||
|
return Math.hypot(ax, ay) + Math.min(Math.max(qx, qy), 0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- design ----------
|
||||||
|
const DOME_C = [0.5, 0.56]; // dome circle center
|
||||||
|
const DOME_R = 0.295; // dome radius
|
||||||
|
const BLADE_C = [0.5, 0.475]; // aperture center
|
||||||
|
const BLADE_R = 0.155;
|
||||||
|
const BLADE_W = 0.048;
|
||||||
|
const ARC_C = [0.5, 0.53];
|
||||||
|
const ARC_R = 0.4;
|
||||||
|
const ARC_W = 0.036;
|
||||||
|
|
||||||
|
const NAVY = [10, 52, 128];
|
||||||
|
const BLUE_HI = [110, 196, 250];
|
||||||
|
const BLUE_LO = [9, 68, 152];
|
||||||
|
const WHITE = [248, 252, 255];
|
||||||
|
const ORANGE = [240, 128, 60];
|
||||||
|
const GREEN = [110, 190, 74];
|
||||||
|
const YELLOW = [240, 204, 60];
|
||||||
|
|
||||||
|
function domeColor(px, py) {
|
||||||
|
const t = Math.pow(clamp(Math.hypot(px - 0.34, py - 0.36) / 0.5, 0, 1), 0.8);
|
||||||
|
let c = mix3(BLUE_HI, BLUE_LO, t);
|
||||||
|
// darken slightly toward the base for depth
|
||||||
|
const base = clamp((py - 0.42) / 0.16, 0, 1) * 0.18;
|
||||||
|
return [c[0] * (1 - base), c[1] * (1 - base), c[2] * (1 - base)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function bladeSDF(px, py) {
|
||||||
|
let best = 1e9;
|
||||||
|
const span = 100 * DEG;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const a0 = (-90 + i * 120) * DEG;
|
||||||
|
best = Math.min(best, arcSDF(px, py, BLADE_C[0], BLADE_C[1], BLADE_R, a0, a0 + span, BLADE_W));
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
function arcsSDF(px, py) {
|
||||||
|
return [
|
||||||
|
{ sdf: arcSDF(px, py, ARC_C[0], ARC_C[1], ARC_R, -160 * DEG, -20 * DEG, ARC_W), color: ORANGE },
|
||||||
|
{ sdf: arcSDF(px, py, ARC_C[0], ARC_C[1], ARC_R, 105 * DEG, 170 * DEG, ARC_W), color: GREEN },
|
||||||
|
{ sdf: arcSDF(px, py, ARC_C[0], ARC_C[1], ARC_R, 10 * DEG, 75 * DEG, ARC_W), color: YELLOW },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(size) {
|
||||||
|
const SS = 4;
|
||||||
|
const R = size * SS;
|
||||||
|
const out = Buffer.alloc(size * size * 4);
|
||||||
|
const px = 1 / R;
|
||||||
|
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
|
for (let x = 0; x < size; x++) {
|
||||||
|
let r = 0, g = 0, b = 0, a = 0;
|
||||||
|
for (let sy = 0; sy < SS; sy++) {
|
||||||
|
for (let sx = 0; sx < SS; sx++) {
|
||||||
|
const nx = (x * SS + sx + 0.5) / R;
|
||||||
|
const ny = (y * SS + sy + 0.5) / R;
|
||||||
|
const aa = (sdf) => clamp(0.5 - sdf / px, 0, 1);
|
||||||
|
|
||||||
|
let cr = 0, cg = 0, cb = 0, ca = 0;
|
||||||
|
const over = (sdf, col, opacity) => {
|
||||||
|
const cov = aa(sdf) * opacity;
|
||||||
|
cr = cr * (1 - cov) + col[0] * cov;
|
||||||
|
cg = cg * (1 - cov) + col[1] * cov;
|
||||||
|
cb = cb * (1 - cov) + col[2] * cov;
|
||||||
|
ca = ca * (1 - cov) + cov;
|
||||||
|
};
|
||||||
|
|
||||||
|
// base bar
|
||||||
|
over(rrSDF(nx, ny, 0.5, DOME_C[1] + 0.022, DOME_R + 0.045, 0.028, 0.026), NAVY, 1);
|
||||||
|
// dome (upper half of circle)
|
||||||
|
const dd = Math.max(Math.hypot(nx - DOME_C[0], ny - DOME_C[1]) - DOME_R, ny - DOME_C[1]);
|
||||||
|
over(dd, domeColor(nx, ny), 1);
|
||||||
|
// aperture blades
|
||||||
|
over(bladeSDF(nx, ny), WHITE, 0.95);
|
||||||
|
// colored arcs
|
||||||
|
for (const arc of arcsSDF(nx, ny)) over(arc.sdf, arc.color, 1);
|
||||||
|
|
||||||
|
r += cr; g += cg; b += cb; a += ca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const n = SS * SS;
|
||||||
|
const o = (y * size + x) * 4;
|
||||||
|
out[o] = Math.round(clamp(r / n, 0, 255));
|
||||||
|
out[o + 1] = Math.round(clamp(g / n, 0, 255));
|
||||||
|
out[o + 2] = Math.round(clamp(b / n, 0, 255));
|
||||||
|
out[o + 3] = Math.round(clamp(a / n, 0, 1) * 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- main ----------
|
||||||
|
const outDir = process.argv[2] || '.';
|
||||||
|
const sizes = [16, 24, 32, 48, 64, 128, 256];
|
||||||
|
const images = sizes.map((s) => ({ size: s, png: pngEncode(s, s, render(s)) }));
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(outDir, 'obs.png'), images[images.length - 1].png);
|
||||||
|
fs.writeFileSync(path.join(outDir, 'obs-studio.ico'), icoEncode(images));
|
||||||
|
console.log('wrote obs.png (256x256) and obs-studio.ico (' + sizes.join(',') + ') to ' + outDir);
|
||||||