Skill 04 · Dreambase Industrial Schematics
Subchapter 4.1
references/recipes.mdMarkdown11 KBView on GitHub
Everything is plain SVG (optionally wrapped in HTML/CSS for motion). No libraries, no canvas, no 3D engine.
A tilted circle is an ellipse. For a ring of radius r, tilted tilt
degrees away from the viewer and rotated rot degrees in the screen plane,
a point at ring-angle θ (0° = 12 o’clock, clockwise) projects to:
function project(angleDeg, r, tilt, rot, cx, cy) {
const a = ((angleDeg - 90) * Math.PI) / 180; // 0° at top, clockwise
const x0 = r * Math.cos(a);
const y0 = r * Math.sin(a) * Math.cos((tilt * Math.PI) / 180); // squash
const rr = (rot * Math.PI) / 180; // in-plane rotation
return {
x: cx + x0 * Math.cos(rr) - y0 * Math.sin(rr),
y: cy + x0 * Math.sin(rr) + y0 * Math.cos(rr),
depth: (Math.sin(a) + 1) / 2, // 0 = far side (top), 1 = near side
};
}tilt 55–70° gives the cinematic ellipse in the references; 0° is a
face-on dial.rot −10° to −25° keeps the composition dynamic (avoid 0° — it looks
like a chart).depth for both opacity (0.15 + 0.85 * depth^1.2) and to split
elements into a far group (blurred) and near group (sharp).r, inner point at r - tickLen, both projected
with the same function. Never draw ticks as a dasharray on the ellipse —
they must point at the center to read as instrumentation.scripts/industrial-ring.mjs implements all of this; prefer it for scaffolds.
<filter id="glow" x="-300%" y="-300%" width="700%" height="700%">
<feGaussianBlur stdDeviation="4" result="b1"/>
<feGaussianBlur in="SourceGraphic" stdDeviation="1.2" result="b2"/>
<feMerge>
<feMergeNode in="b1"/><feMergeNode in="b2"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>Apply to: the bright progress arc, active node dots, nothing else. The arc
is a sampled path along the ellipse between two ring-angles, stroke
= ink, stroke-width 3–5, stroke-linecap="round", drawn twice — once
wide at 25% opacity (halo), once thin at full opacity (core).
At ring-angle θ (matching Linear-style references):
fill="none", stroke ink at 70% opacity,
stroke-width 1.2 — sits exactly on the ring line.filter="url(#glow)", offset ~6px
toward the outside of the ring (it “leads” the open circle).text-anchor start on the right
half, end on the left half.Inactive nodes: drop the glow dot, dim the open circle to 35%.
Split the scene into <g id="far"> and <g id="near"> by depth < 0.45.
<filter id="dof"><feGaussianBlur stdDeviation="2.2"/></filter>
<!-- far group: filter="url(#dof)" opacity="0.5" -->
<filter id="grain">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
<feComponentTransfer><feFuncA type="linear" slope="0.05" intercept="0"/></feComponentTransfer>
</filter>
<!-- topmost layer: <rect width="100%" height="100%" filter="url(#grain)"/> -->Optional vignette: a radial gradient rect from transparent center to 60%
ground color at the corners. Blurring a few labels (not just ticks) at
the far side sells the camera-focus effect — set filter="url(#dof)" on
far-side text too.
Think of the object as a stack of cylinders on one axis (a camera lens: hood → barrel → knurled focus ring → mount). Draw in this order:
--industrial-steel-2 path (two ellipse arcs joined by straight walls).
Vary radii aggressively: fat housing, thin collar, fat ring. Real
hardware alternates.--industrial-steel-1 (or the ground color itself for deep gaps). No strokes;
form is shape-on-shape.--industrial-steel-3.--industrial-rim slivers (2–6px) only where the
light hits: the top-left arc of each ellipse edge, the lit long edge of
the barrel. This is the money layer; if it’s on both sides it’s wrong.--industrial-signal) or
a few --industrial-signal-hot marks. One accent zone per composition.Separate segments along the axis vector only — never sideways. Gaps grow
slightly toward the viewer end (e.g. 40, 52, 68px). Keep every part’s
ellipse aspect identical. Optionally add 0.75px --industrial-ink-faint hairlines
connecting mating faces, and park a ruler strip in a lower corner as a
“calibration” prop.
A rounded-rect track (--industrial-steel-1 fill, or transparent on black) with
40–80 vertical tick rects (1.5–2px wide, evenly spaced): completed ticks in
--industrial-signal at 60% opacity, remaining ticks in --industrial-ink-faint, one
active tick full-height in --industrial-signal-hot or ink with glow. Generate
with industrial-ring.mjs ruler. As an HTML component, the same reads as a
progress/scrubber bar; animate the active index with a CSS custom property.
ground rect → vignette → far group (blurred) → ring/machinery/mesh faces → near group → mesh lines → labels/callouts → glow elements → grain rect. Grain is always last.
The two-parameter tilt in §1 is a special case. For arbitrary camera angles, work in a screen-style frame (x right, y down, z toward the viewer), rotate with Euler angles applied Rx → Ry → Rz, then project:
function rotXYZ({x, y, z}, r) { // r = {x, y, z} in degrees
const R = (d) => (d * Math.PI) / 180;
let c = Math.cos(R(r.x)), s = Math.sin(R(r.x));
[y, z] = [y * c - z * s, y * s + z * c]; // Rx: tilt away/toward
c = Math.cos(R(r.y)); s = Math.sin(R(r.y));
[x, z] = [x * c + z * s, -x * s + z * c]; // Ry: swing left/right
c = Math.cos(R(r.z)); s = Math.sin(R(r.z));
[x, y] = [x * c - y * s, x * s + y * c]; // Rz: spin in-plane
return { x, y, z };
}
// perspective: d = camera distance in px (1200–2000 feels like the refs;
// 0 or Infinity = orthographic/isometric)
const f = d > 0 ? d / (d - p.z) : 1;
screen = { x: cx + p.x * f, y: cy + p.y * f };Model objects flat in the XY plane (z = 0 top surface, z = −depth bottom),
then rotate. The classic reference camera is {x: 55–65, y: 0, z: −10 to −20}; straight-on 2D is {x: 0, y: 0, z: 0} with perspective: 0.
Depth cues must follow the rotation: map each element’s post-rotation z to
opacity (near bright, far dim) rather than hardcoding “top half is far”.
scripts/industrial-mesh.mjs donut implements all of this; read its
--help for the config. The construction, for building other mesh objects
by hand:
rotXYZ, no
projection).wire — no fills at all; the mesh is fully transparent (pure
hologram).hybrid — every face filled with a near-black theme tint at ~0.85
opacity, back-faces at ~half that; drawn painter-sorted (far → near).
This is the reference look: you just see through the object.cel — opaque flat shading: cull back-faces (normal z ≤ 0), sort,
fill by quantized light b = dot(N, L) into shadow/mid/light, and
stroke the top edge of well-lit wall quads (b > 0.55) with the rim
color. Family B’s shading on a data object.2D meshes are the same pipeline with depth: 0 (top face only); pies are
innerR: 0.
Derive an entire scene from one hue: convert the hue to HSL, keep H fixed,
and produce every role by varying S/L only (implemented in
industrial-mesh.mjs as deriveTheme):
| Role | S | L |
|---|---|---|
| line / glow | 0.75 | 0.62 |
| line-dim (secondary labels, grid) | 0.40 | 0.40 |
| rim (cel lit edges) | 0.80 | 0.82 |
| fill shadow | 0.30 | 0.05 |
| fill mid | 0.32 | 0.09 |
| fill light | 0.30 | 0.14 |
| background | 0.30 | 0.025 |
The background carrying a trace of the hue (not pure black) is what makes the scene feel lit by its own phosphor. Use the same derivation for dashboard micro-panels, chart bars, and text in the scene — a stray neutral gray reads as a compositing mistake.
The labeling system for mesh/machinery objects: anchor dot on the object → short 45° diagonal → horizontal rule → label sitting on the rule.