commit 0a1e6c384edf270037104b023df24fb184b57499 Author: planetrenox@protonmail.com Date: Wed Oct 21 20:24:42 2026 +0000 Add renox-as-planet logo design The word Renox, wrapped around a 3D sphere as raised continents, with clouds, an ice cap, an atmosphere, a ring and a moon. Generated with opentype.js and rendered to PNG with resvg. Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..229e9c5 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Planet Renox logos + +Logo designs for Planet Renox, written as code. Each design sits in its own folder under `designs/` and writes its SVG and PNG files next to its script. + +```sh +bun install +bun run build +``` + +## designs/renox-as-planet + +![Renox as a planet](designs/renox-as-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. + +| 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`). diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..cd729d5 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/designs/renox-as-planet/index.js b/designs/renox-as-planet/index.js new file mode 100644 index 0000000..a67c70d --- /dev/null +++ b/designs/renox-as-planet/index.js @@ -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 ? `` : '' +}).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) => ``).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 = ` + + ${RINGS.flatMap(([a, b, o], _, __, m = ceil((b - a) / .005)) => Array.from({ length: m }, (_, i) => { + const r = a + (b - a) * (i + .5) / m + return `` + })).join('')} + ` + +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 + ? `` + : `` +}).join('') + +const lx = n(C + L[0] * R * .62), ly = n(C - L[1] * R * .62) +const svg = bg => ` + + + + + + + + + + + + + + + + + + + + + +${bg ? ` + + +${stars}` : ''} + + + + + + + + + + + + + ${relief} + + + ${clouds} + + + + +${ring} +` + +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()) +} diff --git a/designs/renox-as-planet/logo-transparent.png b/designs/renox-as-planet/logo-transparent.png new file mode 100644 index 0000000..84ad5f0 Binary files /dev/null and b/designs/renox-as-planet/logo-transparent.png differ diff --git a/designs/renox-as-planet/logo-transparent.svg b/designs/renox-as-planet/logo-transparent.svg new file mode 100644 index 0000000..2efbf62 --- /dev/null +++ b/designs/renox-as-planet/logo-transparent.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/designs/renox-as-planet/logo.png b/designs/renox-as-planet/logo.png new file mode 100644 index 0000000..981af6a Binary files /dev/null and b/designs/renox-as-planet/logo.png differ diff --git a/designs/renox-as-planet/logo.svg b/designs/renox-as-planet/logo.svg new file mode 100644 index 0000000..ea906a0 --- /dev/null +++ b/designs/renox-as-planet/logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4af66f3 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "logo", + "private": true, + "type": "module", + "description": "Planet Renox logo designs, written as code", + "dependencies": { + "@fontsource/unbounded": "^5.3.0", + "@resvg/resvg-js": "^2.6.2", + "opentype.js": "^2.0.0" + }, + "scripts": { + "build": "bun designs/renox-as-planet/index.js" + } +}