nvrsion: Add, refactor, and fix: use ./app-logo-canary.icon for canary builds in BUILD.md, app-logo-canary.icon/icon.json, and scripts/package-app.sh; refactor chat logic to use advanced technologies in website/index.html; adjust chat atom animation to model realism with 2d lines and circles vanishing at angles; resolve '--build-system native' deprecation in ./app-logo-canary.icon/icon.json; adjust make BUMP= defaults to ‘build’ for canary builds, releases, and local dev in Makefile and scripts/release-macos.sh.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-02 02:10:08 -07:00
co-authored by nucleic
parent cf6ce35ea4
commit b3c7b1ce44
4 changed files with 178 additions and 100 deletions
+5 -4
View File
@@ -85,15 +85,16 @@ dmg-stable:
./scripts/make-dmg.sh stable
# Releases bump ./VERSION: build number always +1, plus a semver bump via BUMP. Default is minor,
# except the fast-moving canary channel which defaults to patch. BUMP overrides either.
# except the fast-moving canary/dev channels which default to build-only (no marketing-version
# change). BUMP overrides either.
# make release-beta # 0.1.0 (827) -> 0.2.0 (828): minor (the default)
# make release-canary # 0.1.0 (827) -> 0.1.1 (828): patch (canary's default)
# make release-canary # 0.1.0 (827) -> 0.1.0 (828): build-only (canary's default)
# make release-beta BUMP=major # -> 1.0.0 (828)
# make release-beta BUMP=patch # -> 0.1.1 (828)
# make release-canary BUMP=minor # -> 0.2.0 (828): override canary's patch default
# make release-canary BUMP=patch # -> 0.1.1 (828): override canary's build-only default
# make release-beta BUMP=build # -> 0.1.0 (828): build number only
## release-canary: signed + notarized + stapled DMG release (canary channel). BUMP=major|minor|patch (default: patch)
## release-canary: signed + notarized + stapled DMG release (canary channel). BUMP=major|minor|patch|build (default: build)
release-canary:
NUCLEIC_BUMP=$(BUMP) ./scripts/release-macos.sh canary
+2 -2
View File
@@ -41,8 +41,8 @@
"value" : false
}
],
"image-name" : "logo-v1-dark.png",
"name" : "logo-v1-dark",
"image-name" : "logo-v1-canary.png",
"name" : "logo-v1-canary",
"position" : {
"scale" : 0.25,
"translation-in-points" : [
+9 -8
View File
@@ -13,14 +13,14 @@
# 'nucleic-notary') OR the NUCLEIC_ASC_* API-key env vars (see scripts/notarize.sh).
#
# Versioning: every release bumps the build number (CFBundleVersion) by 1 in ./VERSION, and
# bumps a semver component (CFBundleShortVersionString) — default minor (patch on the fast-moving
# canary channel), since we're pre-1.0. The new ./VERSION is committed on success (so versions/build
# bumps a semver component (CFBundleShortVersionString) — default minor (build-only on the
# fast-moving canary and dev channels), since we're pre-1.0. The new ./VERSION is committed on success (so versions/build
# numbers stay monotonic). Both the Makefile shortcut `make release-beta BUMP=major` and a direct
# `NUCLEIC_BUMP=build` work.
#
# Env:
# NUCLEIC_SIGN_ID signing identity (default: auto-detect Developer ID Application)
# NUCLEIC_BUMP semver bump: minor (default; patch on canary) | major | patch | build (build = build number only)
# NUCLEIC_BUMP semver bump: minor (default; build on canary/dev) | major | patch | build (build = build number only)
# NUCLEIC_VERSION_COMMIT commit the ./VERSION bump on success (default 1; 0 leaves it unstaged)
# NUCLEIC_VERSION pin the marketing version (overrides ./VERSION; forwarded to package-app.sh)
# NUCLEIC_NOTARIZE_APP notarize+staple the .app too (default 1; the DMG is always done)
@@ -65,18 +65,19 @@ export NUCLEIC_SIGN_ID="$SIGN_ID"
echo "▸ Releasing $CHANNEL with identity: $SIGN_ID"
# 0. Bump the version: build number always +1, plus a semver bump (NUCLEIC_BUMP, default minor —
# we're pre-1.0, so each release is a minor, except canary which defaults to a patch; major stays
# 0 until an explicit BUMP=major).
# we're pre-1.0, so each release is a minor, except the canary/dev channels which default to
# build-only (no marketing-version change); major stays 0 until an explicit BUMP=major).
# bump-version.sh rewrites ./VERSION; package-app.sh reads it on the very next line. If the
# release fails before the artifact exists, an EXIT trap restores ./VERSION to its committed
# value so a retry reuses the same number instead of skipping one. On success the bump is
# committed (step 7); SUCCEEDED gates the trap so the bump survives.
SUCCEEDED=0
trap '[ "$SUCCEEDED" = 1 ] || git checkout -- VERSION 2>/dev/null || true' EXIT
# Default bump is channel-aware: canary iterates fast so it defaults to a patch; the promotion
# Default bump is channel-aware: the fast-moving canary and dev channels iterate constantly, so
# they default to build-only (build number +1, marketing version unchanged); the promotion
# channels (beta/rc/stable) default to a minor (we're pre-1.0). NUCLEIC_BUMP overrides either —
# e.g. `make release-canary BUMP=minor`.
DEFAULT_BUMP="minor"; [ "$CHANNEL" = "canary" ] && DEFAULT_BUMP="patch"
# e.g. `make release-canary BUMP=patch`.
DEFAULT_BUMP="minor"; case "$CHANNEL" in canary|dev) DEFAULT_BUMP="build" ;; esac
BUMP="${NUCLEIC_BUMP:-$DEFAULT_BUMP}"
read -r REL_VERSION REL_BUILD <<EOF
$(scripts/bump-version.sh "$BUMP")
+162 -86
View File
@@ -171,86 +171,25 @@
border-radius:999px;padding:6px 13px;margin:0 0 6px;
}
/* ---------- atom visual — true CSS 3D ----------
The brand mark rebuilt as a real 3D system: three tilted circular orbits
under perspective (each projects to the brand ellipse), electrons riding
each ring, and the whole system slowly precessing. Every animation here
is transform/opacity only, so it runs composited off the main thread —
no SMIL, no script, no per-frame layout or paint.
NOTE: no opacity/filter on .orbit itself — either would force group
rasterization and disable the plane splitting that lets the rings
visually interlock where they intersect. */
/* ---------- atom visual ----------
Drawn on <canvas> by the small scoped script beside it: orbits are real
circles in 3D, perspective-projected and depth-sorted every frame. The
rasterizer anti-aliases each stroke at any angle, and a constant tube
width means an orbit turning edge-on reads as a wire — it never goes
paper-thin or shimmers the way a flat CSS/SVG ring does. Renders only
while on-screen; prefers-reduced-motion gets a single static frame. */
.atom-wrap{display:flex;justify-content:center;align-items:center;min-width:0}
.atom{
width:100%;max-width:420px;aspect-ratio:1;position:relative;
perspective:1100px;
width:100%;max-width:420px;aspect-ratio:1;height:auto;
/* radial clearing: fades the page's graph-paper dotting out beneath the
atom so the ring geometry and nucleus ping sit on a clean field */
atom so the projected orbits sit on a clean field */
background:radial-gradient(closest-side, var(--paper) 52%, color-mix(in srgb,var(--paper) 55%, transparent) 78%, transparent 100%);
}
.hero-grid>*{min-width:0}
.atom .sys3d{
position:absolute;inset:0;transform-style:preserve-3d;
transform:rotateX(-14deg); /* static pose when animation is off */
animation:precess 44s linear infinite;
animation-delay:-18s;
}
@keyframes precess{
from{transform:rotateX(-14deg) rotateY(0turn)}
to{transform:rotateX(-14deg) rotateY(1turn)}
}
.atom .orbit{
position:absolute;left:15%;top:15%;width:70%;height:70%;
border:5px solid var(--c);border-radius:50%;
transform:rotateZ(var(--rz)) rotateX(68deg);
transform-style:preserve-3d;
}
.atom .orbit.oa{--c:#5e9bd8;--rz:80deg}
.atom .orbit.ob{--c:#db5f97;--rz:20deg}
.atom .orbit.oc{--c:#c47e69;--rz:140deg}
/* spinner rotates in the ring's own plane and carries the electrons;
inset:-2.5px aligns its edges to the stroke's centerline */
.atom .spinner{
position:absolute;inset:-2.5px;transform-style:preserve-3d;
animation:spin var(--dur) linear infinite;animation-delay:var(--ph,0s);
}
@keyframes spin{to{transform:rotate(360deg)}}
.atom .e{
position:absolute;left:50%;top:0;width:16px;height:16px;border-radius:50%;
transform:translate(-50%,-50%);
background:radial-gradient(circle at 32% 30%, color-mix(in srgb,var(--c) 45%, #fff), var(--c) 72%);
box-shadow:0 1px 3px rgba(0,0,0,.18);
}
.atom .e.minor{top:100%;width:10px;height:10px;opacity:.75}
/* nucleus overlay — the fixed point of control the 3D system turns around */
.atom .nucleus3d{
position:absolute;left:50%;top:50%;width:18px;height:18px;border-radius:50%;
background:var(--ink);transform:translate(-50%,-50%);
}
.atom .halo,.atom .ping3d{
position:absolute;left:50%;top:50%;width:32px;height:32px;border-radius:50%;
border:1.5px solid var(--ink);transform:translate(-50%,-50%);
}
.atom .halo{opacity:.25}
.atom .ping3d{opacity:0;animation:ping 5s linear infinite}
@keyframes ping{
from{transform:translate(-50%,-50%) scale(1);opacity:.28}
to{transform:translate(-50%,-50%) scale(4.2);opacity:0}
}
@media (max-width:820px){
.hero{padding:48px 0 32px}
.hero-grid{grid-template-columns:1fr;gap:30px}
.atom{max-width:230px}
.atom .orbit{border-width:3px}
.atom .spinner{inset:-1.5px}
.atom .e{width:9px;height:9px}
.atom .e.minor{width:6px;height:6px}
.atom .nucleus3d{width:11px;height:11px}
.atom .halo,.atom .ping3d{width:19px;height:19px}
}
@media (prefers-reduced-motion:reduce){
.atom .sys3d,.atom .spinner{animation:none}
.atom .e,.atom .ping3d{display:none}
}
/* ---------- premise band ---------- */
@@ -954,23 +893,160 @@
</div>
<div class="atom-wrap">
<!-- the brand atom in real 3D: agents (electrons) orbit one nucleus of
control while the whole system precesses. Each tilted ring projects
to the brand ellipse under perspective; rings interlock via CSS
plane splitting. Spinner delays are negative so every orbit is
mid-flight at first paint. -->
<div class="atom" role="img" aria-label="Coding agents orbiting one point of control">
<div class="sys3d">
<div class="orbit oa"><div class="spinner" style="--dur:13s;--ph:-5s"><i class="e"></i><i class="e minor"></i></div></div>
<div class="orbit ob"><div class="spinner" style="--dur:17s;--ph:-6s"><i class="e"></i><i class="e minor"></i></div></div>
<div class="orbit oc"><div class="spinner" style="--dur:21s;--ph:-3s"><i class="e"></i><i class="e minor"></i></div></div>
</div>
<!-- nucleus ping — the control point broadcasting to its fleet -->
<div class="ping3d"></div>
<div class="halo"></div>
<div class="nucleus3d"></div>
</div>
<canvas id="atom-cv" class="atom" role="img" aria-label="Coding agents orbiting one point of control"></canvas>
<!-- static brand mark if scripting is unavailable -->
<noscript>
<style>#atom-cv{display:none}</style>
<svg class="atom" viewBox="0 0 400 400" aria-hidden="true">
<g fill="none" stroke-width="5">
<ellipse cx="200" cy="200" rx="140" ry="58" transform="rotate(80 200 200)" stroke="#5e9bd8"/>
<ellipse cx="200" cy="200" rx="140" ry="58" transform="rotate(20 200 200)" stroke="#db5f97"/>
<ellipse cx="200" cy="200" rx="140" ry="58" transform="rotate(140 200 200)" stroke="#c47e69"/>
</g>
<circle cx="200" cy="200" r="9" fill="#181520"/>
</svg>
</noscript>
</div>
<script>
/* Hero atom — a real 3D model rendered on canvas. Orbits are circles in
3D (68° inclination, three azimuths), electrons ride them, the nucleus
is a rotating cluster of nucleons, and the whole system precesses.
Everything is perspective-projected and depth-sorted per frame
(painter's algorithm), so orbits and electrons genuinely pass behind
and in front of the nucleus. Canvas rasterization keeps strokes
anti-aliased at every angle, unlike a flat ring turned edge-on.
Perf: one 2D canvas capped at 2× DPR, ~300 primitives/frame; the rAF
loop runs only while the canvas is on-screen, and
prefers-reduced-motion gets a single static frame. */
(() => {
const cv = document.getElementById("atom-cv");
const ctx = cv && cv.getContext && cv.getContext("2d");
if (!ctx) return;
const TAU = Math.PI * 2, TILT = 1.187; // 68° orbital inclination
const ORB = [ // azimuth ≈ brand angles 80/20/140°
{ c: [94, 155, 216], az: 1.396, T: 13, ph: [0.0, 2.6] },
{ c: [219, 95, 151], az: 0.349, T: 17, ph: [1.2, 4.1] },
{ c: [196, 126, 105], az: 2.443, T: 21, ph: [3.4, 5.9] },
];
// nucleon cluster: unit-ish directions, 1 = proton (ink), 0 = neutron (grey)
const NUC = [
[0, 0, 0, 1], [.95, .2, .1, 0], [-.85, .4, .3, 1], [.3, -.9, .35, 0],
[-.3, .8, -.55, 0], [.5, .5, -.7, 1], [-.55, -.6, -.55, 1], [.1, -.35, .9, 0],
];
const SEG = 88, items = [];
let W = 0, raf = 0, visible = false;
const rgba = (c, a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;
function draw(t) {
const R = W * 0.35, f = W * 2.6, cx = W / 2, cy = W / 2;
const pre = t * TAU / 44; // 44s precession
const cp = Math.cos(pre), sp = Math.sin(pre);
const cb = Math.cos(-0.24), sb = Math.sin(-0.24); // fixed camera tilt
const rot = (x, y, z) => { // rotY(pre) then rotX(-14°)
const x1 = x * cp + z * sp, z1 = z * cp - x * sp;
return [x1, y * cb - z1 * sb, y * sb + z1 * cb];
};
ctx.clearRect(0, 0, W, W);
items.length = 0;
for (const o of ORB) {
// orbit basis vectors: circle in xy → Rx(TILT) → Rz(az) → world
const ca = Math.cos(o.az), sa = Math.sin(o.az);
const cT = Math.cos(TILT), sT = Math.sin(TILT);
const u = rot(ca, sa, 0);
const v = rot(-sa * cT, ca * cT, sT);
const pt = (a) => {
const co = Math.cos(a), si = Math.sin(a);
const x = R * (u[0] * co + v[0] * si), y = R * (u[1] * co + v[1] * si),
z = R * (u[2] * co + v[2] * si);
const s = f / (f - z);
return [cx + x * s, cy + y * s, z, s];
};
const P = [];
for (let i = 0; i <= SEG; i++) P.push(pt(i / SEG * TAU));
for (let i = 0; i < SEG; i++)
items.push({ k: 0, z: (P[i][2] + P[i + 1][2]) / 2, o, a: P[i], b: P[i + 1], R });
for (const ph of o.ph) {
const a = ph + t * TAU / o.T, p = pt(a), tr = [];
for (let j = 1; j <= 8; j++) tr.push(pt(a - j * 0.07));
items.push({ k: 1, z: p[2], o, p, tr, R });
}
}
// nucleus — cluster spins slowly on its own axis on top of the precession
const cq = Math.cos(t * 0.5), sq = Math.sin(t * 0.5);
const rc = W * 0.02, rn = W * 0.0225;
for (const [dx0, dy, dz0, kind] of NUC) {
const dx = dx0 * cq + dz0 * sq, dz = dz0 * cq - dx0 * sq;
const p = rot(dx * rc, dy * rc, dz * rc);
const s = f / (f - p[2]);
items.push({ k: 2, z: p[2], x: cx + p[0] * s, y: cy + p[1] * s, r: rn * s, kind });
}
items.sort((p, q) => p.z - q.z); // far → near
ctx.lineCap = "butt";
for (const it of items) {
if (it.k === 0) { // orbit tube segment
const d = (it.z / it.R + 1) / 2; // 0 far … 1 near
ctx.strokeStyle = rgba(it.o.c, 0.22 + 0.6 * d);
ctx.lineWidth = W * 0.012 * it.a[3];
ctx.beginPath(); ctx.moveTo(it.a[0], it.a[1]); ctx.lineTo(it.b[0], it.b[1]); ctx.stroke();
} else if (it.k === 1) { // electron: trail, glow, core
const [x, y, , s] = it.p;
const d = (it.z / it.R + 1) / 2, re = W * 0.017 * s;
ctx.lineCap = "round";
for (let j = 0; j < it.tr.length - 1; j++) {
const q = 1 - j / it.tr.length;
ctx.strokeStyle = rgba(it.o.c, 0.28 * q * q * (0.3 + 0.7 * d));
ctx.lineWidth = re * 1.5 * q;
ctx.beginPath(); ctx.moveTo(it.tr[j][0], it.tr[j][1]);
ctx.lineTo(it.tr[j + 1][0], it.tr[j + 1][1]); ctx.stroke();
}
ctx.lineCap = "butt";
let g = ctx.createRadialGradient(x, y, 0, x, y, re * 2.8);
g.addColorStop(0, rgba(it.o.c, 0.4 * (0.4 + 0.6 * d)));
g.addColorStop(1, rgba(it.o.c, 0));
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(x, y, re * 2.8, 0, TAU); ctx.fill();
g = ctx.createRadialGradient(x - re * 0.35, y - re * 0.35, re * 0.15, x, y, re);
g.addColorStop(0, rgba([255, 255, 255], 0.9));
g.addColorStop(0.35, rgba(it.o.c, 1));
g.addColorStop(1, rgba(it.o.c, 0.9));
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(x, y, re, 0, TAU); ctx.fill();
} else { // nucleon sphere
const base = it.kind ? [24, 21, 32] : [122, 114, 128];
const hi = it.kind ? [90, 82, 104] : [178, 171, 184];
const g = ctx.createRadialGradient(
it.x - it.r * 0.35, it.y - it.r * 0.35, it.r * 0.1, it.x, it.y, it.r);
g.addColorStop(0, rgba(hi, 1)); g.addColorStop(1, rgba(base, 1));
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(it.x, it.y, it.r, 0, TAU); ctx.fill();
}
}
}
const frame = (ts) => { draw(ts / 1000); raf = requestAnimationFrame(frame); };
const rm = matchMedia("(prefers-reduced-motion: reduce)");
function update() {
const want = visible && !rm.matches && W > 0;
if (want && !raf) raf = requestAnimationFrame(frame);
else if (!want && raf) { cancelAnimationFrame(raf); raf = 0; }
if (!raf && W > 0) draw(25.3); // static pose
}
new ResizeObserver(() => {
const dpr = Math.min(devicePixelRatio || 1, 2);
W = cv.clientWidth;
cv.width = cv.height = Math.round(W * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!raf) update();
}).observe(cv);
new IntersectionObserver((es) => {
visible = es[0].isIntersecting; update();
}).observe(cv);
rm.addEventListener && rm.addEventListener("change", update);
})();
</script>
</div>
</section>