@patricksurry/g3
Version:
A flexible Javascript framework for building steam gauge instrument panels that display live external metrics from flight (or other) simulators like XPlane or MS FS2020
224 lines (210 loc) • 11.9 kB
JavaScript
import * as d3 from 'd3';
import * as g3 from '../g3.js';
export const
// format a heading in degrees like 270 => W, 240 => 24
headingFormat = (v) => (v%90==0)?'NESW'.charAt(v/90):(v/10),
deviationScale = d3.scaleLinear().domain([-10,10]).range([-50,50]);
export function altitudeGeneric() {
return g3.gauge()
.metric('altitude').unit('ft')
.fake(g3.forceSeries(0, 30000, {fmax: 0.001}))
.measure(d3.scaleLinear().domain([0, 1000]).range([0, 360]))
.defs(
g3.element('pattern', {
id: 'altitudeDangerPattern',
width: 10, height: 10,
patternTransform: 'rotate(45)',
patternUnits: 'userSpaceOnUse'
}).append(g3.element('rect', {width: 10, height: 5}).class('g3-fg-fill')),
)
.append(
// self-indicating gauge for pressure, to view through window
g3.put().rotate(90).append(
g3.gauge()
.metric('pressureSetting').unit('hPa')
.fake(g3.forceSeries(955, 1075))
.measure(d3.scaleLinear().domain([955,1075]).range([0, 360]))
.autoindicate(true)
.append(
g3.gaugeFace().style('fill: #111'),
g3.axisTicks().step(5).size(5).inset(10),
g3.axisTicks().step(1).size(3).inset(10),
g3.axisTicks([1013.25]).size(5).inset(10).class('g3-danger-stroke'),
g3.axisLabels(d3.range(955, 1075, 5)).size(10).orient('relative').rotate(-90),
)
),
// add an inner circle with danger hatching
g3.element('circle', {r: 65, fill: 'url(#altitudeDangerPattern)'}),
// rotating cover for danger stripes, behind the main face
// see https://www.cfinotebook.net/notebook/avionics-and-instruments/altimeter
// A striped segment is visible below 10,000', when mask starts to cover, fully covered at 15,000'
g3.indicatePointer().rescale(v => 3*v/100).append(
g3.axisSector([625, 1125]).inset(40).size(60).class('g3-bg-fill')
).style('filter: url(#dropShadow1)'),
// add a face with two see-through windows
g3.gaugeFace().window((_, g) => {
g3.axisSector([225, 275]).inset(11).size(24).style('fill: black')(_, g); // pressure window
g3.axisSector([425, 575]).inset(40).size(40).style('fill: black')(_, g); // low-altitude danger window
}).style('filter: url(#dropShadow3)'), //TODO doesn't drop shadow windows properly?
g3.axisTicks([250]).shape('wedge').size(10).width(3),
g3.axisTicks().step(20),
g3.axisTicks().step(100).size(15).style('stroke-width: 2'),
g3.axisLabels().step(100).format(v => v/100).size(25),
g3.axisLabels({950: '100', 50: 'FEET'}).orient('relative').size(8).inset(16),
// g3.gaugeLabel("ALTITUDE").y(-10).size(15),
g3.axisLabels({0: '1000 FEET'}).orient('clockwise').inset(50).size(8),
g3.axisLabels({0: '10000 FEET'}).orient('clockwise').inset(62).size(8),
g3.gaugeLabel("CALIBRATED").x(-40).y(-8).size(6),
g3.gaugeLabel("TO").x(-40).y(0).size(6),
g3.gaugeLabel("20,000 FEET").x(-40).y(8).size(6),
g3.indicatePointer().shape('dagger').rescale(v => v/100),
g3.indicatePointer().shape('blade').rescale(v => v/10),
g3.indicatePointer().shape('sword'),
);
}
export function attitudeGeneric() {
return g3.gauge()
// outer gauge is invisible other than the indicator marks
.append(
// the outer dial is a self-indicating roll gauge
g3.gauge()
.metric('roll').unit('deg')
.fake(g3.forceSeries(-25, 25))
.measure(d3.scaleLinear().domain([-90,90]).range([-90,90]))
.autoindicate(true)
.clip(g3.gaugeFace())
.append(
// the inner dial is a self-indicating pitch gauge with a linear scale
g3.put().rotate(-90).append(
g3.gauge()
.metric('pitch').unit('deg')
.fake(g3.forceSeries(-25, 25))
.measure(d3.scaleLinear().domain([-20, 20]).range([-25,25]))
.kind('linear').autoindicate(true)
.css('text {fill: #ddd}')
.append(
g3.element('rect', {height: 200, width: 150, y: -100}) // sky
.class('g3-sky-fill g3-fg-stroke'),
g3.element('rect', {height: 200, width: 150, x: -150, y: -100}) // ground
.class('g3-ground-fill g3-fg-stroke'),
g3.axisTicks([-20,20]).size(50).inset(-25),
g3.axisTicks([-10,10]).size(30).inset(-15),
g3.axisTicks().start(-15).step(10).size(20).inset(-10),
g3.axisLabels([-20,20]).inset(35).format(Math.abs).rotate(90).size(12),
g3.axisLabels([-20,20]).inset(-35).format(Math.abs).rotate(90).size(12),
g3.axisLabels([-10,10]).inset(25).format(Math.abs).rotate(90).size(12),
g3.axisLabels([-10,10]).inset(-25).format(Math.abs).rotate(90).size(12),
)
),
g3.axisSector([0, 360]).size(20).style('filter: url(#dropShadow3)'),
g3.axisSector([-90,90]).size(20).class('g3-sky-fill g3-fg-stroke'),
g3.axisSector([90,270]).size(20).class('g3-ground-fill g3-fg-stroke'),
g3.axisTicks().step(30).size(20),
g3.axisTicks([-45, 45]).shape('wedge').inset(10).size(10).width(10),
g3.axisTicks([-20,-10,10,20]).inset(10).size(10),
),
// add outline, post-clipping to avoid losing half the exterior
g3.element('circle', {r: 100}).class('g3-fg-stroke'),
g3.put().append(
// top pointer
g3.element('path', {d: 'M 0,-80 l 5,15 l -10,0 z'})
.class('g3-highlight-stroke g3-no-fill'),
// grey handlebar support
g3.element('path', {d: 'M -15,0 a 15,10 0 0 0 30,0 M 0,10 l 0,35'})
.class('g3-bg-stroke').style('stroke-width: 2'),
g3.element('path', {d: 'M 0,40 l 15,38 a 100,100 0 0 1 -30,0 z'})
.class('g3-bg-fill').style('stroke-width: 2'),
// handlebar arms and dot
g3.element('path', {d: 'M -50,0 l 35,0 m 30,0 l 35,0'})
.class('g3-highlight-stroke g3-no-fill').style('filter: url(#dropShadow1)'),
g3.element('circle', {r: 2}).class('g3-highlight-fill'),
).style('filter: url(#dropShadow1)')
);
}
export function headingGeneric() {
return g3.gauge()
.metric('heading').unit('deg')
.fake(g3.forceSeries(0, 360, {wrap: true}))
.measure(d3.scaleLinear().domain([0, 360]).range([0, 360]))
.autoindicate(true)
.append(
g3.gaugeFace(),
g3.axisTicks().step(5).size(5),
g3.axisTicks().step(10).size(10),
g3.axisTicks().step(30).size(10).style('stroke-width: 2'),
g3.axisLabels().step(30).orient('relative').format(headingFormat),
g3.indicatePointer().shape('aircraft-heading'),
);
}
export function VSIGeneric() {
return g3.gauge()
.metric('verticalSpeed').unit('ft/min')
.fake(g3.forceSeries(-2500, 2500))
.measure(d3.scaleLinear().domain([-2000, 2000]).range([90, 90+360]))
.append(
g3.gaugeFace(),
g3.axisTicks().step(100).size(5),
g3.axisTicks().step(500).size(15).style('stroke-width: 2'),
g3.axisLabels().step(1000).format(v => Math.abs(v/100)),
g3.axisLabels().step(1000).start(-1500).format(v => Math.abs(v/100)).size(16),
g3.gaugeLabel("VERTICAL SPEED").y(-25).size(12),
g3.gaugeLabel("100 FEET PER MIMUTE").y(-10).size(10),
// add up/down arrows as sector lines plus custom tick marks
g3.axisSector([100,400]).size(0).inset(25).class('g3-fg-stroke').style('stroke-width: 2'),
g3.axisSector([-100,-400]).size(0).inset(25).class('g3-fg-stroke').style('stroke-width: 2'),
// TODO this should be a custom tick mark shape, with access to current tick val?
(_, g) => {
_.selectAll(null).data([-400, 400])
.enter().append('path')
.attr('d', "M 2,0 l -7,4 l 0,-8 Z")
.attr('transform', d => g.marktransform(d, 25) + (d < 0 ? ' rotate(-180)':''))
.attr('class', 'g3-fg-fill')
},
g3.gaugeLabel("UP").x(-57).y(-37).size(8).style('text-anchor: start'),
g3.gaugeLabel("DOWN").x(-57).y(37).size(8).style('text-anchor: start'),
g3.indicatePointer().shape('sword').clamp([-1950, 1950]),
);
}
export function turnCoordinatorGeneric() {
return g3.gauge()
.metric('turnrate').unit('deg/s')
.fake(g3.forceSeries(-10, 10))
.measure(d3.scaleLinear().domain([-3, 3]).range([-20, 20]))
.append(
// gaguge for slip, degrees of ball deflection
g3.gauge()
.metric('slip').unit('deg')
.fake(g3.forceSeries(-25, 25))
.measure(d3.scaleLinear().domain([-20,20]).range([170,190]))
.r(300)
.append(
g3.gaugeFace(),
g3.gaugeLabel('TURN COORDINATOR').size(11).y(22),
g3.gaugeLabel('2 MIN').size(14).y(65),
g3.put().y(-255).append(
// background for the tube
g3.axisSector([-30,30]).inset(-9).size(18).style('fill: #c0c0a8; filter: url(#dropShadow3)'),
// add ball as pointer
g3.indicatePointer().clamp([-20,20]).append(
g3.put().scale(0.8,1).y(-300).rotate(-180).append(
// add ball background plus a highlight, with the rotate(-180) to avoid having
// the highlight flipped since the ball is tyipcally indicating near 180 rotation
g3.element('circle', {r: 8}).style('fill: #333'),
g3.element('circle', {r: 8}).class('g3-highlight'),
)
),
g3.axisTicks([-3,3]).inset(-9).size(18).style('stroke-width: 2; stroke: #666'),
// add a blurred highlight to the tube
g3.axisSector([-30,30]).inset(0).size(6)
.style('fill: white; filter: url(#gaussianBlur1); fill-opacity: 0.33')
)
),
g3.axisSector([0,360]).size(24).class('g3-bg-fill').style('filter: url(#dropShadow2)'),
g3.axisTicks([-16.5, -13.5, 13.5, 16.5]).size(24).style('stroke-width: 4'),
g3.gaugeLabel('D.C.').y(-92),
g3.gaugeLabel('ELEC.').y(-82),
g3.gaugeLabel('NO PITCH').y(80).size(8),
g3.gaugeLabel('INFORMATION').y(90).size(8),
g3.indicatePointer().clamp([-9,9]).shape('aircraft-turn'),
);
}