svg-icons
An icon proxy in the style of tosijs-ui's icons, over tosijs-3d's own
focused icon set. svgIcons.<name>() returns an SVG ElementCreator — so you
get the full power of the elements factory (props, bindings, event handlers,
children), not just a static blob. Because it's a real element it works both as a
flat DOM node and — via .outerHTML — as markup rasterized into an in-scene
SVG texture (the VR panels), so one icon set serves both presentations.
Demo
The whole set, plus a few composed variants (rotate / flip / opacity / a
generator redirect). color sets currentColor, which the stroked glyphs follow
and the baked-color cube ignores.
import { svgIcons, iconNames } from 'tosijs-3d'
import { elements } from 'tosijs'
const { div } = elements
const tile = (name) =>
div(
{
style:
'display:flex;flex-direction:column;align-items:center;gap:6px;width:88px;color:#dfe6ef;font:12px system-ui',
},
svgIcons[name]({ style: { height: '40px', color: '#5fb0ff' } }),
div(name)
)
const names = [
...iconNames(),
'arrowUpRight90r', // rotate: ↗ → ↘
'arrowUpRight0f', // flip H: ↗ → ↖ (a rotation can't produce this)
'camera50o', // 50% opacity
'arrowDownRight', // a generator redirect (→ arrowUpRight90r)
]
preview.style.overflow = 'auto'
preview.append(
div(
{
style:
'display:flex;flex-wrap:wrap;gap:18px;padding:20px;background:#141821;border-radius:12px',
},
...names.map(tile)
)
)
Composition suffixes
Names carry a subset of tosijs-ui's suffix grammar (see icon-name) — the pure style modifiers: rotate/flip/scale/translate/opacity/stroke-width/color. Directional redirects baked in by the icon generator resolve too:
svgIcons.arrowUpRight90r() // rotate: ↗ → ↘
svgIcons.arrowUpRight0f() // flip H: ↗ → ↖ (a rotation can't produce this)
svgIcons.arrowDownRight() // generator redirect → arrowUpRight rotated 90°
svgIcons.camera50o() // camera at 50% opacity
svgIcons.xrColor() // the purpose-built "enter XR / VR" mark
Not implemented (deliberately): $ stacking and the spin/un/check overlay
rule-prefixes.
The set
The artwork lives in icons/{color,stroked,filled}/*.svg; bun run icons
regenerates icon-data via tosijs-ui's tosijs-make-icons generator (folder
name → default fill/stroke/color handling). Add an SVG, rerun, done. Notable
marks: tosijs3d (the brand cube), xrColor (the enter-XR/VR affordance, from
tosijs-ui), and tosiXr.
DOM or 3D — svgIcons vs iconGlyph
The same icons two ways, side by side: svgIcons in the DOM (left) and
iconGlyph baked onto an in-scene SVG texture (right). The 3D
grid proves the raster path — currentColor doesn't resolve there, so iconGlyph
bakes explicit colours (here #e6e6e6), while color icons keep their palette.
Orbit the plane; they should match the DOM grid.
import { b3d, b3dLight, b3dSvgPlane, svgIcons, iconGlyph, iconNames } from 'tosijs-3d'
import { svgElements, elements } from 'tosijs'
const { svg, rect } = svgElements
const { div } = elements
const names = iconNames().slice(0, 12)
const cols = 4
const cell = 64
const iconPx = 40
const rows = Math.ceil(names.length / cols)
const w = cols * cell
const h = rows * cell
// 3D side: one SVG sheet of iconGlyphs (explicit colours) → plane texture.
const sheet = svg(
{ viewBox: `0 0 ${w} ${h}`, width: w, height: h },
rect({ x: 0, y: 0, width: w, height: h, fill: '#141821' }),
...names.map((n, i) =>
iconGlyph(n, {
color: '#e6e6e6',
size: iconPx,
x: (i % cols) * cell + (cell - iconPx) / 2,
y: Math.floor(i / cols) * cell + (cell - iconPx) / 2,
})
)
)
const plane = b3dSvgPlane({
width: 2.4,
height: (2.4 * h) / w,
resolution: 512,
materialChannel: 'emissive',
pointerEvents: false,
})
plane.svgElement = sheet
const scene = b3d(
{
// <tosi-b3d> has no intrinsic size — it MUST be given one or it collapses to
// zero height and the canvas renders blank (a snapshot() still works, since
// that renders offscreen, so size it explicitly for the on-page view).
style: 'width:320px;height:300px;display:block;border-radius:8px;overflow:hidden',
sceneCreated(el) {
const cam = new el.BABYLON.ArcRotateCamera(
'cam', -Math.PI / 2, Math.PI / 2.6, 3.2, el.BABYLON.Vector3.Zero(), el.scene
)
el.setActiveCamera(cam)
cam.attachControl(el.scene.getEngine().getRenderingCanvas(), true)
},
},
b3dLight({ intensity: 1 }),
plane
)
// DOM side: the same icons via svgIcons.
const domGrid = div(
{
style: `display:grid;grid-template-columns:repeat(${cols},${cell}px);background:#141821;color:#e6e6e6`,
},
...names.map((n) =>
div(
{ style: `display:flex;align-items:center;justify-content:center;height:${cell}px` },
svgIcons[n]({ style: { height: `${iconPx}px` } })
)
)
)
preview.append(
div(
{ style: 'display:flex;gap:24px;align-items:flex-start;padding:16px;background:#0c0e14' },
div({ style: 'color:#9ab;font:12px system-ui' }, 'DOM (svgIcons)', domGrid),
div({ style: 'color:#9ab;font:12px system-ui' }, '3D texture (iconGlyph)', scene)
)
)