Move renox-as-planet into self-contained versions/blue

Each design version now carries its own package.json, lockfile, script, README and rendered files, so versions can be built and changed independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:28:05 +00:00
co-authored by Claude Opus 5.5
parent 0a1e6c384e
commit 8ed908bf1d
9 changed files with 27 additions and 17 deletions
@@ -0,0 +1,17 @@
# Renox as a planet: blue
![Renox as a planet](logo.png)
The word **Renox** is the planet. The script uses [opentype.js](https://github.com/opentypejs/opentype.js) to turn the word, set in Unbounded Black, into vector outlines, and splits every curve into many short segments. It then wraps each point around a 3D sphere with an orthographic projection. That way the letters curve over the globe and get squeezed toward its edges like real continents. Stacked shells raise the letters off the surface, and gradients light the whole planet from one light source. Clouds, an ice cap, an atmosphere glow, a few islands, a ring and a moon finish it off. [resvg](https://github.com/thx/resvg-js) renders the PNGs.
```sh
bun install
bun run build
```
| file | what |
| --- | --- |
| `logo.svg` / `logo.png` | on a deep-space background |
| `logo-transparent.svg` / `logo-transparent.png` | planet only, transparent background |
The settings at the top of `index.js` control the camera tilt (`T`), how far the word wraps around the planet (`SPAN`), how high the letters rise (`RISE`), the letter spacing (`TRACK`), the light direction (`L`) and the ring bands (`RINGS`).
@@ -0,0 +1,45 @@
{
"lockfileVersion": 2,
"configVersion": 1,
"workspaces": {
"": {
"name": "logo",
"dependencies": {
"@fontsource/unbounded": "^5.3.0",
"@resvg/resvg-js": "^2.6.2",
"opentype.js": "^2.0.0",
},
},
},
"packages": {
"@fontsource/unbounded": ["@fontsource/unbounded@5.3.0", "", {}, "sha512-pPD6hsTnjoQRr6pCsqMHbHBfauB4LZygCNQEUJHu+CO2wzDFHqELEFSG7sTwo5Z5Gr4q3OK0fIILOXBJDv6lcw=="],
"@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="],
"@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="],
"@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.6.2", "", { "os": "android", "cpu": "arm64" }, "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ=="],
"@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A=="],
"@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw=="],
"@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.6.2", "", { "os": "linux", "cpu": "arm" }, "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw=="],
"@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg=="],
"@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg=="],
"@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw=="],
"@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ=="],
"@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ=="],
"@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.6.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w=="],
"@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="],
"opentype.js": ["opentype.js@2.0.0", "", { "bin": { "ot": "bin/ot" } }, "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A=="],
}
}
@@ -0,0 +1,144 @@
// Renox as a planet: the word's glyph outlines are wrapped onto a sphere as raised
// continents, then lit, given an atmosphere, clouds, an ice cap and a ring.
import opentype from 'opentype.js'
import { Resvg } from '@resvg/resvg-js'
import { readFileSync, writeFileSync } from 'fs'
const at = p => new URL(p, import.meta.url)
const fb = readFileSync(at('node_modules/@fontsource/unbounded/files/unbounded-latin-900-normal.woff'))
const font = opentype.parse(fb.buffer.slice(fb.byteOffset, fb.byteOffset + fb.byteLength))
const { sin, cos, hypot, ceil, min, max, PI } = Math
const D = PI / 180, S = 1200, C = S / 2, R = 300
const T = 18 * D // camera elevation above the equator
const SPAN = 66 * D // half the longitude the word wraps around
const LAT = T // word latitude, puts it dead centre of the disc
const RISE = .03 // continent height as a fraction of R
const TRACK = 4 // letter spacing in em/100
const L = (v => v.map(c => c / hypot(...v)))([-.55, .62, .56]) // light, view space
const RINGS = [[1.3, 1.37, .25], [1.39, 1.58, .7], [1.61, 1.64, .3], [1.67, 1.8, .5], [1.82, 1.85, .22]]
const n = v => +v.toFixed(1)
const rnd = (s => () => (s = (s * 16807) % 2147483647) / 2147483647)(7)
// lon/lat on a sphere of radius r -> screen [x, y, depth] (depth > 0 faces the camera)
const P = (lon, lat, r = 1) => {
const x = cos(lat) * sin(lon), y = sin(lat), z = cos(lat) * cos(lon)
return [C + R * r * x, C - R * r * (y * cos(T) - z * sin(T)), y * sin(T) + z * cos(T)]
}
const poly = pts => 'M' + pts.map(([x, y]) => n(x) + ' ' + n(y)).join('L')
// lay out the word, then flatten every outline into dense polylines so straight
// strokes bend properly once they are wrapped round the sphere
const cmds = []
const gs = font.stringToGlyphs('RENOX')
gs.reduce((x, g, i) => {
cmds.push(...g.getPath(x, 0, 100).commands)
return x + (g.advanceWidth + (gs[i + 1] ? font.getKerningValue(g, gs[i + 1]) : 0)) / font.unitsPerEm * 100 + TRACK
}, 0)
const bez = (q, t) => q.length < 2 ? q[0] : bez(q.slice(1).map((v, i) => [q[i][0] + (v[0] - q[i][0]) * t, q[i][1] + (v[1] - q[i][1]) * t]), t)
const outlines = []
let cur, p
for (const c of cmds) {
if (c.type == 'Z') continue
const q = [p, c.x1 != null && [c.x1, c.y1], c.x2 != null && [c.x2, c.y2], [c.x, c.y]].filter(Boolean)
if (c.type == 'M') outlines.push(cur = [[c.x, c.y]])
else for (let i = 1, k = max(2, ceil(q.slice(1).reduce((s, v, j) => s + hypot(v[0] - q[j][0], v[1] - q[j][1]), 0))); i <= k; i++) cur.push(bez(q, i / k))
p = [c.x, c.y]
}
const xs = outlines.flat().map(v => v[0]), ys = outlines.flat().map(v => v[1])
const xm = (min(...xs) + max(...xs)) / 2, ym = (min(...ys) + max(...ys)) / 2
const k = SPAN / ((max(...xs) - min(...xs)) / 2)
const word = r => outlines.map(o => poly(o.map(([x, y]) => P((x - xm) * k, LAT + (ym - y) * k * cos(LAT), r))) + 'Z').join('') + islands(r)
// clouds: soft streaks drifting along latitudes, kept clear of the word
const clouds = Array.from({ length: 16 }, () => {
const lat = (rnd() < .5 ? -46 + rnd() * 32 : 40 + rnd() * 20) * D, lon = (rnd() * 240 - 120) * D, len = (18 + rnd() * 36) * D
const amp = (rnd() - .5) * 5 * D, w = n(4 + rnd() * 8), o = .2 + rnd() * .25
const pts = Array.from({ length: 48 }, (_, i) => P(lon + len * i / 47, lat + amp * sin(i / 47 * PI), 1 + RISE * 1.2)).filter(v => v[2] > .1)
return pts.length > 3 ? `<path d="${poly(pts)}" stroke-width="${w * 2.4}" opacity="${n(o * 50) / 100}" filter="url(#haze)"/><path d="${poly(pts)}" stroke-width="${w}" opacity="${n(o * 100) / 100}" filter="url(#soft)"/>` : ''
}).join('')
// islands: a small archipelago so the letters read as continents
const isles = [[-50, 45, 2.4], [44, 47, 1.7], [-38, -31, 3], [-24, -37, 1.3], [34, -34, 2.2], [50, -27, 1.2]]
.map(([lon, lat, rad]) => [lon * D, lat * D, rad * D, rnd() * 9])
const islands = r => isles.map(([lon, lat, rad, ph]) => poly(Array.from({ length: 48 }, (_, i) => {
const a = i / 48 * 2 * PI, q = rad * (1 + .2 * sin(3 * a + ph) + .1 * sin(5 * a + ph * 2))
return P(lon + q * cos(a) / cos(lat), lat + q * sin(a), r)
})) + 'Z').join('')
// relief: stack shells from the sea floor up to the plateau, darkest first
const mix = (a, b, t) => '#' + [0, 2, 4].map(i => n(parseInt(a.slice(i + 1, i + 3), 16) * (1 - t) + parseInt(b.slice(i + 1, i + 3), 16) * t) | 0).map(v => v.toString(16).padStart(2, '0')).join('')
const relief = Array.from({ length: 9 }, (_, i) => `<path d="${word(1 + RISE * i / 9)}" fill="${mix('#4a1530', '#d2453a', i / 8)}"/>`).join('')
const cap = poly(Array.from({ length: 180 }, (_, i) => P(i * 2 * D, (75 + 1.6 * sin(i * 2 * D * 5) + .9 * sin(i * 2 * D * 13 + 1)) * D, 1 + RISE * .6))) + 'Z'
const ring = `<g id="ring" transform="translate(${C} ${C}) scale(1 ${n(sin(T) * 1000) / 1000})">
<g fill="none" stroke="url(#ringTone)">
${RINGS.flatMap(([a, b, o], _, __, m = ceil((b - a) / .005)) => Array.from({ length: m }, (_, i) => {
const r = a + (b - a) * (i + .5) / m
return `<circle r="${n(r * R)}" stroke-width="${n((b - a) / m * R + .6)}" opacity="${n(o * (.55 + rnd() * .45) * 100) / 100}"/>`
})).join('')}
</g></g>`
const stars = Array.from({ length: 170 }, () => {
const x = n(rnd() * S), y = n(rnd() * S), r = rnd() ** 3 * 2.2 + .5, o = n(.25 + rnd() * .75)
return r > 2.1
? `<path d="M${x} ${y - r * 4}Q${x} ${y} ${x + r * 4} ${y}Q${x} ${y} ${x} ${y + r * 4}Q${x} ${y} ${x - r * 4} ${y}Q${x} ${y} ${x} ${y - r * 4}Z" fill="#fff" opacity="${o}"/>`
: `<circle cx="${x}" cy="${y}" r="${n(r)}" fill="#fff" opacity="${o}"/>`
}).join('')
const lx = n(C + L[0] * R * .62), ly = n(C - L[1] * R * .62)
const svg = bg => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${S} ${S}" width="${S}" height="${S}">
<defs>
<radialGradient id="space" cx=".5" cy=".45" r=".75"><stop offset="0" stop-color="#1a1452"/><stop offset=".55" stop-color="#0a0828"/><stop offset="1" stop-color="#030210"/></radialGradient>
<radialGradient id="ocean" gradientUnits="userSpaceOnUse" cx="${lx}" cy="${ly}" r="${R * 1.6}"><stop offset="0" stop-color="#7f8cff"/><stop offset=".3" stop-color="#4141e0"/><stop offset=".62" stop-color="#22178f"/><stop offset="1" stop-color="#0c0736"/></radialGradient>
<radialGradient id="land" gradientUnits="userSpaceOnUse" cx="${lx}" cy="${ly}" r="${R * 1.5}"><stop offset="0" stop-color="#fff1b8"/><stop offset=".28" stop-color="#ffc25c"/><stop offset=".6" stop-color="#ff7b3d"/><stop offset="1" stop-color="#d43f3a"/></radialGradient>
<radialGradient id="shade" gradientUnits="userSpaceOnUse" cx="${lx}" cy="${ly}" r="${R * 1.95}"><stop offset="0" stop-color="#fff" stop-opacity=".22"/><stop offset=".22" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#04021a" stop-opacity="0"/><stop offset=".78" stop-color="#04021a" stop-opacity=".62"/><stop offset=".92" stop-color="#04021a" stop-opacity=".92"/></radialGradient>
<radialGradient id="rim" cx=".5" cy=".5" r=".5"><stop offset=".8" stop-color="#7fe0ff" stop-opacity="0"/><stop offset=".96" stop-color="#8fe6ff" stop-opacity=".45"/><stop offset="1" stop-color="#d4f7ff" stop-opacity=".85"/></radialGradient>
<radialGradient id="halo" cx=".5" cy=".5" r=".5"><stop offset="${n(100 / 1.14) / 100}" stop-color="#6fd0ff" stop-opacity=".55"/><stop offset=".94" stop-color="#5a8cff" stop-opacity=".12"/><stop offset="1" stop-color="#5a8cff" stop-opacity="0"/></radialGradient>
<radialGradient id="lit" gradientUnits="userSpaceOnUse" cx="${lx}" cy="${ly}" r="${R * 1.9}"><stop offset=".3" stop-color="#fff"/><stop offset=".9" stop-color="#222"/></radialGradient>
<linearGradient id="ringTone" gradientUnits="userSpaceOnUse" x1="${-R * 1.9}" y1="0" x2="${R * 1.9}" y2="0"><stop offset="0" stop-color="#f4f0ff"/><stop offset=".5" stop-color="#c9c2ff"/><stop offset="1" stop-color="#7d74c9"/></linearGradient>
<radialGradient id="moon" cx=".32" cy=".28" r=".85"><stop offset="0" stop-color="#f3eeff"/><stop offset=".5" stop-color="#a99fe0"/><stop offset="1" stop-color="#3a2f7c"/></radialGradient>
<radialGradient id="moonGlow"><stop offset=".55" stop-color="#8fe6ff" stop-opacity=".35"/><stop offset="1" stop-color="#5a8cff" stop-opacity="0"/></radialGradient>
<radialGradient id="moonShade" cx=".3" cy=".26" r=".95"><stop offset=".45" stop-color="#04021a" stop-opacity="0"/><stop offset="1" stop-color="#04021a" stop-opacity=".85"/></radialGradient>
<mask id="litMask" maskUnits="userSpaceOnUse" x="0" y="0" width="${S}" height="${S}"><rect width="${S}" height="${S}" fill="url(#lit)"/></mask>
<clipPath id="disc"><circle cx="${C}" cy="${C}" r="${R}"/></clipPath>
<clipPath id="back"><rect width="${S}" height="${C}"/></clipPath>
<clipPath id="front"><rect y="${C}" width="${S}" height="${C}"/></clipPath>
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2.4"/></filter>
<filter id="haze" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="6"/></filter>
<filter id="shore" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="7"/></filter>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="60"/></filter>
</defs>
${bg ? `<rect width="${S}" height="${S}" fill="url(#space)"/>
<circle cx="${C - 330}" cy="${C - 250}" r="230" fill="#5b3cf5" opacity=".22" filter="url(#glow)"/>
<circle cx="${C + 360}" cy="${C + 300}" r="260" fill="#1fb6ff" opacity=".14" filter="url(#glow)"/>
${stars}` : ''}
<g transform="translate(${C + R * 1.55} ${C - R * 1.2})">
<circle r="${R * .22}" fill="url(#moonGlow)"/>
<circle r="${R * .13}" fill="url(#moon)"/>
<g fill="#3b2f7a" opacity=".35"><ellipse cx="${-R * .04}" cy="${-R * .03}" rx="${R * .028}" ry="${R * .022}"/><ellipse cx="${R * .045}" cy="${R * .02}" rx="${R * .02}" ry="${R * .016}"/><ellipse cx="${-R * .01}" cy="${R * .065}" rx="${R * .015}" ry="${R * .011}"/></g>
<circle r="${R * .13}" fill="url(#moonShade)"/>
</g>
<use href="#ring" clip-path="url(#back)"/>
<circle cx="${C}" cy="${C}" r="${R * 1.14}" fill="url(#halo)" mask="url(#litMask)"/>
<g clip-path="url(#disc)">
<circle cx="${C}" cy="${C}" r="${R}" fill="url(#ocean)"/>
<path d="${word(1)}" fill="none" stroke="#6ff0ff" stroke-width="22" stroke-linejoin="round" opacity=".34" filter="url(#shore)"/>
<path d="${word(1)}" fill="none" stroke="#9ff6ff" stroke-width="7" stroke-linejoin="round" opacity=".5" filter="url(#soft)"/>
${relief}
<path d="${word(1 + RISE)}" fill="url(#land)"/>
<path d="${cap}" fill="#eef8ff"/>
<g fill="none" stroke="#fff" stroke-linecap="round">${clouds}</g>
<circle cx="${C}" cy="${C}" r="${R}" fill="url(#shade)"/>
<circle cx="${C}" cy="${C}" r="${R}" fill="url(#rim)" mask="url(#litMask)"/>
</g>
<use href="#ring" clip-path="url(#front)"/>
<defs>${ring}</defs>
</svg>`
for (const [name, bg] of [['logo', 1], ['logo-transparent', 0]]) {
const s = svg(bg)
writeFileSync(at(name + '.svg'), s)
writeFileSync(at(name + '.png'), new Resvg(s, { fitTo: { mode: 'width', value: 2048 }, background: bg ? '#030210' : undefined }).render().asPng())
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 387 KiB

@@ -0,0 +1,14 @@
{
"name": "renox-as-planet-blue",
"private": true,
"type": "module",
"description": "Renox as a planet, blue ocean version",
"scripts": {
"build": "bun index.js"
},
"dependencies": {
"@fontsource/unbounded": "^5.3.0",
"@resvg/resvg-js": "^2.6.2",
"opentype.js": "^2.0.0"
}
}