Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/agentGuiHeroCarouselTextures.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

195 lines
5.7 KiB
TypeScript

import * as THREE from "three";
const TEXTURE_SIZE = 256;
const RECORD_RADIUS_RATIO = 0.47;
const RECORD_LABEL_RADIUS_RATIO = 0.41;
const UNSELECTED_COVER_SCALE = 0.86;
// Composites each host-provided agent icon into the paper label of a shared
// vinyl-record treatment. The monochrome groove/rim palette is intentionally
// material-specific; the label keeps the host artwork and brand color intact.
export function vinylRecordTexture(
image: HTMLImageElement,
onReadyRender: () => void
): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = TEXTURE_SIZE;
canvas.height = TEXTURE_SIZE;
const context = canvas.getContext("2d");
if (context) {
const center = TEXTURE_SIZE / 2;
const recordRadius = TEXTURE_SIZE * RECORD_RADIUS_RATIO;
const recordMaskRadius = recordRadius - 2;
const labelRadius = recordRadius * RECORD_LABEL_RADIUS_RATIO;
context.clearRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
context.save();
context.beginPath();
context.arc(center, center, recordMaskRadius, 0, Math.PI * 2);
context.clip();
const recordFill = context.createRadialGradient(
center * 0.92,
center * 0.88,
labelRadius,
center,
center,
recordRadius
);
recordFill.addColorStop(0, "rgb(26 26 27)");
recordFill.addColorStop(0.54, "rgb(5 5 6)");
recordFill.addColorStop(0.82, "rgb(18 18 19)");
recordFill.addColorStop(1, "rgb(3 3 4)");
context.fillStyle = recordFill;
context.fillRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
for (
let radius = labelRadius + 4;
radius < recordRadius - 3;
radius += 3.5
) {
const grooveIndex = Math.round((radius - labelRadius) / 3.5);
context.beginPath();
context.arc(center, center, radius, 0, Math.PI * 2);
context.strokeStyle =
grooveIndex % 3 === 0
? "rgb(255 255 255 / 0.12)"
: "rgb(255 255 255 / 0.055)";
context.lineWidth = grooveIndex % 3 === 0 ? 0.7 : 0.45;
context.stroke();
}
context.save();
context.translate(center, center);
context.rotate(-Math.PI / 4);
const sheen = context.createLinearGradient(
-recordRadius,
0,
recordRadius,
0
);
sheen.addColorStop(0, "rgb(255 255 255 / 0)");
sheen.addColorStop(0.42, "rgb(255 255 255 / 0.02)");
sheen.addColorStop(0.5, "rgb(255 255 255 / 0.18)");
sheen.addColorStop(0.58, "rgb(255 255 255 / 0.02)");
sheen.addColorStop(1, "rgb(255 255 255 / 0)");
context.fillStyle = sheen;
context.fillRect(
-recordRadius,
-recordRadius,
recordRadius * 2,
recordRadius * 2
);
context.restore();
context.restore();
context.beginPath();
context.arc(center, center, recordMaskRadius - 0.5, 0, Math.PI * 2);
context.strokeStyle =
getComputedStyle(document.body)
.getPropertyValue("--background-session-flow")
.trim() || "transparent";
context.lineWidth = 2;
context.stroke();
context.save();
context.beginPath();
context.arc(center, center, labelRadius, 0, Math.PI * 2);
context.clip();
const scale = Math.max(
(labelRadius * 2) / image.width,
(labelRadius * 2) / image.height
);
const width = image.width * scale;
const height = image.height * scale;
context.drawImage(
image,
center - width / 2,
center - height / 2,
width,
height
);
context.restore();
context.beginPath();
context.arc(center, center, labelRadius, 0, Math.PI * 2);
context.strokeStyle = "rgb(255 255 255 / 0.2)";
context.lineWidth = 1;
context.stroke();
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = 4;
onReadyRender();
return texture;
}
export function coverImageTexture(
image: HTMLImageElement,
onReadyRender: () => void
): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = TEXTURE_SIZE;
canvas.height = TEXTURE_SIZE;
const context = canvas.getContext("2d");
if (context) {
const radius = 12;
const scale = Math.max(
TEXTURE_SIZE / image.width,
TEXTURE_SIZE / image.height
);
const width = image.width * scale * UNSELECTED_COVER_SCALE;
const height = image.height * scale * UNSELECTED_COVER_SCALE;
const x = (TEXTURE_SIZE - width) / 2;
const y = (TEXTURE_SIZE - height) / 2;
context.clearRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
context.save();
context.beginPath();
context.roundRect(x, y, width, height, radius);
context.clip();
context.drawImage(image, x, y, width, height);
context.restore();
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = 4;
texture.needsUpdate = true;
onReadyRender();
return texture;
}
export function roundedIconTexture(
image: HTMLImageElement,
onReadyRender: () => void,
cornerRadius: number
): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = TEXTURE_SIZE;
canvas.height = TEXTURE_SIZE;
const context = canvas.getContext("2d");
if (context) {
const radius = TEXTURE_SIZE * cornerRadius;
context.beginPath();
context.roundRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE, radius);
context.clip();
const scale = Math.max(
TEXTURE_SIZE / image.width,
TEXTURE_SIZE / image.height
);
const width = image.width * scale;
const height = image.height * scale;
context.drawImage(
image,
(TEXTURE_SIZE - width) / 2,
(TEXTURE_SIZE - height) / 2,
width,
height
);
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = 4;
onReadyRender();
return texture;
}