Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
188 lines
5.9 KiB
JavaScript
188 lines
5.9 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { deflateSync } from 'node:zlib';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { writeFileSync } from 'node:fs';
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const outputDir = join(scriptDir, '..', 'apps', 'desktop', 'resources', 'status');
|
|
|
|
const path = {
|
|
start: [0, 0],
|
|
curve1: {
|
|
controls: [
|
|
[0.03, 0.23],
|
|
[0.11, 0.51],
|
|
],
|
|
end: [0.2, 0.83],
|
|
},
|
|
line1: [0.43, 0.63],
|
|
curve2: {
|
|
controls: [
|
|
[0.49, 0.57],
|
|
[0.57, 0.6],
|
|
],
|
|
end: [0.63, 0.69],
|
|
},
|
|
line2: [0.8, 1.0],
|
|
line3: [1.0, 0.89],
|
|
curve3: {
|
|
controls: [
|
|
[0.86, 0.63],
|
|
[0.69, 0.4],
|
|
],
|
|
end: [0, 0],
|
|
},
|
|
};
|
|
|
|
const gradientStops = [
|
|
[0, [144, 182, 255]],
|
|
[0.55, [73, 126, 247]],
|
|
[1, [71, 97, 228]],
|
|
];
|
|
|
|
function cubicPoint(start, control1, control2, end, t) {
|
|
const u = 1 - t;
|
|
return [
|
|
u ** 3 * start[0] + 3 * u * u * t * control1[0] + 3 * u * t * t * control2[0] + t ** 3 * end[0],
|
|
u ** 3 * start[1] + 3 * u * u * t * control1[1] + 3 * u * t * t * control2[1] + t ** 3 * end[1],
|
|
];
|
|
}
|
|
|
|
function appendCurve(points, start, curve) {
|
|
for (let step = 1; step <= 32; step++) {
|
|
points.push(cubicPoint(start, curve.controls[0], curve.controls[1], curve.end, step / 32));
|
|
}
|
|
}
|
|
|
|
function flattenedPath() {
|
|
const points = [path.start];
|
|
appendCurve(points, path.start, path.curve1);
|
|
points.push(path.line1);
|
|
appendCurve(points, path.line1, path.curve2);
|
|
points.push(path.line2, path.line3);
|
|
appendCurve(points, path.line3, path.curve3);
|
|
return points;
|
|
}
|
|
|
|
function contains(points, x, y) {
|
|
let inside = false;
|
|
for (
|
|
let current = 0, previous = points.length - 1;
|
|
current < points.length;
|
|
previous = current++
|
|
) {
|
|
const [x1, y1] = points[current];
|
|
const [x2, y2] = points[previous];
|
|
const crosses = y1 > y !== y2 > y && x < ((x2 - x1) * (y - y1)) / (y2 - y1) + x1;
|
|
if (crosses) inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
function gradientColor(tIn) {
|
|
const t = Math.min(1, Math.max(0, tIn));
|
|
const upperIndex = gradientStops.findIndex(([position]) => position >= t);
|
|
if (upperIndex <= 0) return gradientStops[0][1];
|
|
const [startPosition, startColor] = gradientStops[upperIndex - 1];
|
|
const [endPosition, endColor] = gradientStops[upperIndex];
|
|
const weight = (t - startPosition) / (endPosition - startPosition);
|
|
return startColor.map((channel, index) => channel + (endColor[index] - channel) * weight);
|
|
}
|
|
|
|
const crcTable = Array.from({ length: 256 }, (_, value) => {
|
|
let crc = value;
|
|
for (let bit = 0; bit < 8; bit++) crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
|
|
return crc >>> 0;
|
|
});
|
|
|
|
function crc32(data) {
|
|
let crc = 0xffffffff;
|
|
for (const byte of data) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
return (crc ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function pngChunk(type, data) {
|
|
const typeBytes = Buffer.from(type, 'ascii');
|
|
const length = Buffer.alloc(4);
|
|
length.writeUInt32BE(data.length);
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(Buffer.concat([typeBytes, data])));
|
|
return Buffer.concat([length, typeBytes, data, crc]);
|
|
}
|
|
|
|
function encodePng(width, height, rgba) {
|
|
const header = Buffer.alloc(13);
|
|
header.writeUInt32BE(width, 0);
|
|
header.writeUInt32BE(height, 4);
|
|
header[8] = 8;
|
|
header[9] = 6;
|
|
const scanlines = Buffer.alloc(height * (1 + width * 4));
|
|
for (let y = 0; y < height; y++) {
|
|
const rowStart = y * (1 + width * 4);
|
|
scanlines[rowStart] = 0;
|
|
rgba.copy(scanlines, rowStart + 1, y * width * 4, (y + 1) * width * 4);
|
|
}
|
|
return Buffer.concat([
|
|
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
pngChunk('IHDR', header),
|
|
pngChunk('IDAT', deflateSync(scanlines, { level: 9 })),
|
|
pngChunk('IEND', Buffer.alloc(0)),
|
|
]);
|
|
}
|
|
|
|
function renderStatusIcon(scale) {
|
|
const dimension = 16 * scale;
|
|
const margin = 2 * scale;
|
|
const glyphSize = 12 * scale;
|
|
const supersampling = 8;
|
|
const polygon = flattenedPath().map(([x, y]) => [margin + x * glyphSize, margin + y * glyphSize]);
|
|
const pixels = Buffer.alloc(dimension * dimension * 4);
|
|
|
|
for (let y = 0; y < dimension; y++) {
|
|
for (let x = 0; x < dimension; x++) {
|
|
const channels = [0, 0, 0];
|
|
let coverage = 0;
|
|
for (let sampleY = 0; sampleY < supersampling; sampleY++) {
|
|
for (let sampleX = 0; sampleX < supersampling; sampleX++) {
|
|
const sx = x + (sampleX + 0.5) / supersampling;
|
|
const sy = y + (sampleY + 0.5) / supersampling;
|
|
if (!contains(polygon, sx, sy)) continue;
|
|
const color = gradientColor((sx - margin + (sy - margin)) / (2 * glyphSize));
|
|
for (let channel = 0; channel < 3; channel++) channels[channel] += color[channel];
|
|
coverage++;
|
|
}
|
|
}
|
|
const offset = (y * dimension + x) * 4;
|
|
if (coverage > 0) {
|
|
for (let channel = 0; channel < 3; channel++) {
|
|
pixels[offset + channel] = Math.round(channels[channel] / coverage);
|
|
}
|
|
pixels[offset + 3] = Math.round((255 * coverage) / supersampling ** 2);
|
|
}
|
|
}
|
|
}
|
|
return encodePng(dimension, dimension, pixels);
|
|
}
|
|
|
|
writeFileSync(join(outputDir, 'cu-status.png'), renderStatusIcon(1));
|
|
writeFileSync(join(outputDir, 'cu-status@2x.png'), renderStatusIcon(2));
|