block by gabrielflorit cfe6b1f7698ce3a62e609564ee1cde10

Curvature speeds

Full Screen

Made with blockup

The car accelerates and brakes depending on the curvature vectors of the Australian Grand Prix circuit. The longer the vector lines, the steeper the curve.

Don’t be fooled by the car’s incredibly life-like speed! It is not!

See my previous work:

script.js

import createTrack from './createTrack.js'
import createDash from './createDash.js'
import drawNormals from './drawNormals.js'
import getCurvature from './getCurvature.js'
import drawDash from './drawDash.js'
import drawCar from './drawCar.js'
import moveCar, { moveCarOneLap } from './moveCar.js'
import constants from './constants.js'
import createTimingsChart from './createTimingsChart.js'
import drawTimingsChart from './drawTimingsChart.js'

const before = Date.now()

// Create the track.
const { g: gCircuit, getCoords, turns } = createTrack({
  container: d3.select('.circuit'),
  track: circuitTracks[0]
})

// Create the timings chart.
const timingsBits = createTimingsChart({
  container: d3.select('.timings'),
  turns
})

// Create curvature data.
const steps = 1000
const delta = 1 / steps
const positionsTemp = d3
  .range(0, 1, delta)
  .concat(1)
  .map(pct => ({
    pct,
    ...getCurvature({ pct, getCoords })
  }))

const alphaExtent = d3.extent(positionsTemp, d => d.curvature.modulus())
const alpha = d3
  .scaleLinear()
  .domain(alphaExtent)
  .range([0, 65])

const positions = positionsTemp
  .map(d => ({
    ...d,
    modulus: d.curvature.modulus()
  }))
  .map(d => ({
    ...d,
    curvature: d.curvature.toUnitVector().multiply(alpha(d.modulus))
  }))

// Create the dash.
const { g: gDash, alpha: alphaDash } = createDash(d3.select('.dash'))

const blankCar = () => ({
  // mph
  number: 1,
  positionIndex: 0,
  measures: [
    {
      pct: 0,
      speed: constants.MAX
    }
  ]
})

const drawSummary = ({ car, elapsed }) => {
  const formatTime = d3.timeFormat('%-M:%S.%L')
  d3.select('.other').html(`
    <div>LAP ${formatTime(elapsed || car.elapsed)}</div>
    <div>MAX ${Math.round(d3.max(car.measures, d => d.speed))} KM/H</div>
    <div>MIN ${Math.round(d3.min(car.measures, d => d.speed))} KM/H</div>
  `)
}

// car = moveCarOneLap({ car, positions })
// drawTimingsChart({ car, ...timingsBits })
// // drawSummary(car)
// drawCar({ car, g: gCircuit, getCoords, positions })

let timer
const doIt = () => {
  let now = 0
  if (timer) {
    timer.stop()
  }
  let car = blankCar()

  // Every tick:
  timer = d3.interval(elapsed => {
    const delta = elapsed - now

    // Draw the car.
    drawCar({ car, g: gCircuit, getCoords })

    // Draw the timings chart.
    drawTimingsChart({ car, ...timingsBits })

    // Draw the dash.
    drawDash({ car, g: gDash, alpha: alphaDash })

    // Draw curvature normals.
    drawNormals({ g: gCircuit, positions, car })

    // Move the car.
    car = moveCar({ car, delta, positions })

    now = elapsed

    if (car.measures.slice(-1)[0].pct >= 1) {
      doIt()
      // timer.stop()
      drawSummary({ car, elapsed })
    }

  })
}

doIt()

index.html

<!DOCTYPE html>
<title>blockup</title>
<link href='https://fonts.googleapis.com/css?family=VT323' rel='stylesheet'>
<link href='dist.css' rel='stylesheet' />
<body>
  <h1>
    <span class='title'>speedy racer</span>
    <span>the car accelerates and brakes depending on the circuit curvature vectors. go speedy go!</span>
  </h1>
  <div class='circuit'>
    <h1>Australian Grand Prix</h1>
  </div>
  <div class='timings'></div>
  <div class='dash'></div>
  <div class='other'></div>
  <script src='sylvester.js'></script>
	<script src='d3.v4.min.js'></script>
  <script src='lodash.min.js'></script>
  <script src='circuits-flatter.js'></script>
	<script src='dist.js'></script>
</body>

circuits-flatter.js

var circuitTracks = [[{"x":753.546,"y":644.215},{"x":604.791,"y":681.341},{"x":497.29300000000006,"y":708.044},{"x":483.8740000000001,"y":708.8149999999999},{"x":470.9910000000001,"y":690.7529999999999},{"x":454.8270000000001,"y":671.175},{"x":424.2860000000001,"y":660.236},{"x":397.93200000000013,"y":661.193},{"x":381.30900000000014,"y":665.624},{"x":339.3410000000001,"y":679.119},{"x":241.04900000000012,"y":700.604},{"x":221.90700000000012,"y":703.5550000000001},{"x":139.39600000000013,"y":708.7080000000001},{"x":111.69700000000013,"y":709.1500000000001},{"x":61.02000000000013,"y":706.44},{"x":49.596,"y":700},{"x":47.48,"y":689.413},{"x":50.605,"y":673.8870000000001},{"x":55.651999999999994,"y":649.321},{"x":51.553999999999995,"y":621.763},{"x":40.562,"y":598.765},{"x":22.505999999999997,"y":584.122},{"x":-13.114,"y":564.2869999999999},{"x":-40.587,"y":549.3779999999999},{"x":-48.745000000000005,"y":543.4279999999999},{"x":-55.580000000000005,"y":527.4179999999999},{"x":-65.354,"y":446.2249999999999},{"x":-61.221,"y":404.5849999999999},{"x":-49.378,"y":346.8599999999999},{"x":-48.75,"y":328.5639999999999},{"x":-48.75,"y":328.5639999999999},{"x":-49.115,"y":280.0729999999999},{"x":-41.84,"y":266.8829999999999},{"x":-21.798000000000002,"y":260.17999999999995},{"x":-0.1460000000000008,"y":248.67099999999994},{"x":19.671,"y":230.54099999999994},{"x":43.442,"y":198.74699999999993},{"x":67.33,"y":181.40499999999992},{"x":92.672,"y":168.99599999999992},{"x":118.503,"y":162.69199999999992},{"x":144.065,"y":162.45999999999992},{"x":199.239,"y":176.9279999999999},{"x":261.597,"y":200.84199999999993},{"x":318.155,"y":220.65199999999993},{"x":325.80499999999995,"y":226.01299999999992},{"x":326.88699999999994,"y":233.9509999999999},{"x":320.11699999999996,"y":263.6259999999999},{"x":324.68399999999997,"y":277.02399999999994},{"x":328.368,"y":284.50199999999995},{"x":328.153,"y":284.25399999999996},{"x":357.954,"y":315.236},{"x":436.978,"y":378.216},{"x":475.152,"y":398.772},{"x":521.252,"y":412.077},{"x":538.501,"y":414.061},{"x":589.313,"y":413.734},{"x":629.251,"y":404.86199999999997},{"x":684.3589999999999,"y":387.37199999999996},{"x":708.2189999999999,"y":378.268},{"x":730.7379999999999,"y":369.765},{"x":738.377,"y":362.503},{"x":742.2729999999999,"y":348.918},{"x":754.6949999999999,"y":320.11},{"x":762.8969999999999,"y":302.028},{"x":769.655,"y":295.159},{"x":788.404,"y":288.93399999999997},{"x":910.443,"y":247.93899999999996},{"x":956.076,"y":240.99399999999997},{"x":1001.582,"y":247.12399999999997},{"x":1158.785,"y":286.051},{"x":1182.028,"y":295.223},{"x":1183.336,"y":301.44100000000003},{"x":1165.914,"y":413.06100000000004},{"x":1158.567,"y":448.324},{"x":1142.578,"y":465.862},{"x":1124.681,"y":471.608},{"x":1002.322,"y":469.969},{"x":993.977,"y":472.945},{"x":990.192,"y":482.81399999999996},{"x":998.598,"y":505.849},{"x":1010.05,"y":542.403},{"x":1009.924,"y":552.133},{"x":1002.761,"y":566.1120000000001},{"x":998.079,"y":575.6360000000001},{"x":986.0459999999999,"y":582.8580000000001},{"x":945.6299999999999,"y":595.663},{"x":842.41,"y":621.948},{"x":753.546,"y":644.215},{"x":753.546,"y":644.215}]];

constants.js

const constants = {
  MIN: 30,
  MAX: 320
}

export default constants

createDash.js

import constants from './constants.js'

const toRadians = d => d * 2 * Math.PI / 360

const createDash = container => {
  const margins = { top: 3, right: 0, bottom: 6, left: 0 }
  const outerWidth = container.node().offsetWidth
  const outerHeight = outerWidth / 2
  const width = outerWidth - margins.right - margins.left
  const height = outerHeight - margins.top - margins.bottom

  const g = container
    .append('svg')
    .attr('width', outerWidth)
    .attr('height', outerHeight)
    .append('g')
    .attr(
      'transform',
      `translate(${width / 2 + margins.left}, ${height + margins.top})`
    )

  // Create tick scale.
  const alpha = d3
    .scaleLinear()
    .domain([0, constants.MAX])
    .range([10, 170])
    // .range([0, 180])
    .nice()

  const tickWidth = 4

  // Create tick data.
  const ticks = d3.range(...alpha.domain(), 50).concat(alpha.domain()[1])

  // Define arc.
  const arc = d3.arc()
    .innerRadius(height)
    .outerRadius(height)
    .startAngle(toRadians(alpha.range()[0]))
    .endAngle(toRadians(alpha.range()[1]))

  // Draw arc.
  g.append('path')
    .attr('class', 'arc')
    .attr('d', arc)
    .attr('transform', 'rotate(-90)')

  g.append('text')
    .attr('class', 'label')
    .text('KM/H')
    .attr('text-anchor', 'middle')
    .attr('dy', -40)

  // Draw tick texts.
  g
    .selectAll('text.tick')
    .data(ticks)
    .enter()
    .append('text')
    .attr('class', 'tick')
    .attr('text-anchor', 'middle')
    .attr('dy', 6)
    .attr(
      'transform',
      d =>
        `rotate(${alpha(d)}) translate(${-height * 0.85} 0) rotate(${-alpha(
          d
        )} 0 0)`
    )
    .text(d => d)

  // Draw tick lines.
  g
    .selectAll('line.tick')
    .data(ticks)
    .enter()
    .append('line')
    .attr('class', 'tick')
    .attr('transform', d => `rotate(${alpha(d)}) translate(${-height} 0)`)
    .attr('x2', tickWidth)

  // Draw dial anchor.
  g.append('circle')
    .attr('class', 'dial')
    .attr('r', 5)

  // Draw dial.
  g.append('line')
    .attr('class', 'dial')
    .attr('x1', 0)
    .attr('y1', 0)
    .attr('x2', -height + tickWidth * 1.5)
    .attr('y2', 0)
    .attr('transform', `rotate(${alpha.range()[0]})`)

  return { g, alpha }
}

export default createDash

createTimingsChart.js

import constants from './constants.js'

const createTimingsChart = ({ container, turns }) => {
  const margins = { top: 45, right: 5, bottom: 20, left: 45 }
  const width = container.node().offsetWidth - margins.right - margins.left
  const height = width / 4.5

  const svg = container
    .append('svg')
    .attr('width', width + margins.right + margins.left)
    .attr('height', height + margins.top + margins.bottom)

  const g = svg
    .append('g')
    .attr('transform', `translate(${margins.left}, ${margins.top})`)

  const x = d3
    .scaleLinear()
    .range([0, width])
    .domain([0, 1])

  const y = d3
    .scaleLinear()
    .range([height, 0])
    .domain([0, constants.MAX])
    .nice()

  const line = d3
    .line()
    .curve(d3.curveBasisOpen)
    .x(d => x(d.pct % 1))
    .y(d => y(d.speed))

  const xAxis = g
    .append('g')
    .attr('class', 'axis axis--x')
    .attr('transform', `translate(0, ${height})`)
    .call(
      d3
        .axisBottom(x)
        .tickSize(0)
        .ticks([])
    )

  xAxis
    .append('text')
    .text('Finish')
    .attr('x', x(1))
    .attr('text-anchor', 'end')
    .attr('dy', 14)

  const yAxis = g
    .append('g')
    .attr('class', 'axis axis--y')
    .call(
      d3
        .axisLeft(y)
        .tickSize(0)
        .ticks(5)
    )

  yAxis
    .append('text')
    .text('(KM/H)')
    .attr('dx', -3)
    .attr('dy', 6 - 18)
    .attr('text-anchor', 'end')

  yAxis
    .append('text')
    .text('Speed')
    .attr('dx', -3)
    .attr('dy', 6 - 18 - 18)
    .attr('text-anchor', 'end')

  g
    .selectAll('line.turn')
    .data(turns)
    .enter()
    .append('line')
    .attr('class', 'turn')
    .attr('transform', d => `translate(${x(d.pct)} 0)`)
    .attr('y2', height)

  g
    .selectAll('text.turn')
    .data(turns)
    .enter()
    .append('text')
    .attr('class', 'turn')
    .attr('transform', d => `translate(${x(d.pct)} 0)`)
    .attr('text-anchor', 'middle')
    .text(d => (d.number === 1 ? 'Turn ' : '') + d.number)
    .attr('dy', -8)

  return {
    g,
    x,
    y,
    line
  }
}

export default createTimingsChart

createTrack.js

import getAngle from './getAngle.js'

const drawFinishLine = ({ g, getCoords }) => {
  const delta = 0.01
  const before = getCoords({ pct: 1 - delta })
  const zero = getCoords({ pct: 0 })
  const after = getCoords({ pct: delta })

  g
    .append('line')
    .attr('class', 'finish-line')
    .attr('x1', before[0])
    .attr('y1', before[1])
    .attr('x2', after[0])
    .attr('y2', after[1])
    .attr('transform', `rotate(90, ${zero})`)
}

const drawTrack = ({ container, track }) => {
  const margins = { top: 10, right: 20, bottom: 30, left: 20 }
  const dimension = container.node().offsetWidth - margins.right - margins.left

  const data = track.map(d => [d.x, d.y])

  const xExtent = d3.extent(data, d => d[0])
  const x = d3.scaleLinear().domain(xExtent)

  const yExtent = d3.extent(data, d => d[1])
  const y = d3.scaleLinear().domain(yExtent)

  const aspect = (xExtent[1] - xExtent[0]) / (yExtent[1] - yExtent[0])

  const width = Math.min(dimension * aspect, dimension)
  const height = Math.min(dimension / aspect, dimension)

  x.range([0, width])
  y.range([0, height])

  const svg = container
    .append('svg')
    .attr('width', width + margins.right + margins.left)
    .attr('height', height + margins.top + margins.bottom)

  const g = svg.append('g').attr('transform', `translate(${margins.left}, ${margins.top})`)

  const line = d3
    .line()
    .curve(d3.curveBasis)
    .x(d => x(d[0]))
    .y(d => y(d[1]))

  const path = g
    .append('path')
    .attr('class', 'track')
    .datum(data)
    .attr('d', line)
    .node()

  const totalLength = path.getTotalLength()

  const getCoords = ({ pct = 0, delta = 0 }) => {
    const length = totalLength * pct + delta
    const pxy = path.getPointAtLength(length % totalLength)
    const qxy = path.getPointAtLength((length + 1) % totalLength)
    const p = [pxy.x, pxy.y]
    const q = [qxy.x, qxy.y]
    const angle = getAngle([p, q])

    const array = [...p]
    array.angle = angle
    return array
  }

  // Draw finish line.
  drawFinishLine({ g, getCoords })

  // Create turn coords.
  const turns = [
    { number: 1, pct: 0.0825, dx: 0.02, dy: 0.07 },
    { number: 3, pct: 0.22, dx: 0.01, dy: -0.01 },
    { number: 5, pct: 0.282, dx: 0.01, dy: -0.01 },
    { number: 6, pct: 0.36, dx: 0.01, dy: 0.04 },
    { number: 9, pct: 0.496, dx: -0.025, dy: 0.01, textAnchor: 'end' },
    { number: 11, pct: 0.637, dx: 0.01, dy: -0.03, textAnchor: 'end' },
    { number: 13, pct: 0.8, dx: -0.01, dy: 0.02, textAnchor: 'end' },
    { number: 14, pct: 0.845, dx: -0.01, dy: -0.01, textAnchor: 'end' },
    { number: 15, pct: 0.9, dx: 0.01, dy: 0.015 },
  ].map(d => ({
    ...d,
    coords: getCoords({ pct: d.pct }),
    dx: (xExtent[1] - xExtent[0]) * (d.dx || 0) + xExtent[0],
    dy: (yExtent[1] - yExtent[0]) * (d.dy || 0) + yExtent[0]
  }))

  g
    .selectAll('text.turn')
    .data(turns)
    .enter()
    .append('text')
    .attr('class', 'turn')
    .attr('transform', d => `translate(${d.coords})`)
    .attr('text-anchor', d => d.textAnchor || 'begin')
    .attr('dx', d => x(d.dx))
    .attr('dy', d => y(d.dy))
    .text(d => (d.number === 1 ? 'Turn ' : '') + (d.number))

  return { g, getCoords, turns }
}

export default drawTrack

dist.css

*{box-sizing:border-box}html{background:rgba(1,11,20,.5);padding:0;margin:0}body{background:#010b14;font-family:VT323,monospace;overflow:hidden;margin:0 auto;padding:.5em;position:relative;max-width:960px;max-height:500px}svg{display:block}h1{color:#dacbed;text-transform:uppercase;font-weight:400;font-style:italic;float:left;text-align:center;padding:0;margin:.5em 0 0 0;width:25%;font-size:2em}h1 span{text-shadow:0 0 14px rgba(218,203,237,.75);display:inline-block}h1 span.title{text-shadow:0 0 14px rgba(249,69,51,.75);font-size:1.5em;color:#f94533}.other{position:absolute;bottom:.5em;color:#018dae}.timings{width:75%;margin-left:25%;position:relative}.timings svg{-webkit-filter:drop-shadow(0 0 14px rgba(15,208,254,.5));filter:drop-shadow(0 0 14px rgba(15,208,254,.5))}.timings svg path.car{fill:none;stroke:#0fd0fe}.timings svg line.turn{stroke:#018dae;stroke-dasharray:1 5}.timings svg text.turn{fill:#018dae;font-size:1.25em}.timings svg circle.car{fill:#f94533}.timings svg .axis path{stroke:#018dae;stroke-dasharray:1 10}.timings svg .axis text{font-size:1.75em;font-family:VT323,monospace;fill:#018dae}.dash{width:25%;position:absolute;bottom:5em;left:.5em}.dash svg{-webkit-filter:drop-shadow(0 0 14px rgba(15,208,254,.5));filter:drop-shadow(0 0 14px rgba(15,208,254,.5))}.dash path.arc{stroke:#dacbed}.dash line.dial{stroke:#f94533;stroke-width:2px}.dash circle.dial{fill:#018dae}.dash text{fill:#018dae;font-size:1.25em}.dash text.label{font-size:.9em}.dash line{stroke:#dacbed}.circuit{width:60%;float:left;position:relative;margin:0 0 0 7.5%;padding:0}.circuit h1{position:absolute;font-size:1.25em;width:100%;text-align:center;top:7em;color:#018dae;text-shadow:0 0 14px rgba(1,141,174,.75)}.circuit svg{-webkit-filter:drop-shadow(0 0 14px #dacbed);filter:drop-shadow(0 0 14px #dacbed);overflow:visible}.circuit svg path.track{fill:none;stroke:#0fd0fe;opacity:.2}.circuit svg path.car{fill:#f94533}.circuit svg circle{fill:#f94533}.circuit svg circle.turn{fill:#f94533}.circuit svg line.normal{stroke:#dacbed}.circuit svg line.finish-line{stroke:#dacbed;stroke-width:2px}.circuit svg text{fill:#dacbed}.circuit svg text.turn{fill:#018dae;font-size:1.25em}

drawCar.js

const drawCar = ({ car, g, getCoords }) => {
  const data = [car].map(d => getCoords({ pct: d.measures.slice(-1)[0].pct }))

  const triangles = g.selectAll('path.car').data(data)

  triangles
    .enter()
    .append('path')
    .attr('class', 'car')
    .attr('d', d =>
      d3
        .symbol()
        .type(d3.symbolTriangle)
        .size(100)()
    )
    .merge(triangles)
    .attr(
      'transform',
      d =>
        `rotate(${-d.angle +
          90} ${d[0]} ${d[1]}) translate(${d[0]} ${d[1]}) scale(1 1.5)`
    )

  triangles.exit().remove()
}

export default drawCar

drawDash.js

const drawDash = ({ car, g, alpha }) => {
  g.select('line.dial')
    .attr('transform', `rotate(${alpha(car.measures.slice(-1)[0].speed)})`)
}

export default drawDash

drawNormals.js

const drawNormals = ({ g, positions, car }) => {
  const lines = g
    .selectAll('line.normal')
    .data(positions.slice(0, car.positionIndex + 1))

  lines
    .attr('x2', d => d.curvature.elements[0])
    .attr('y2', d => d.curvature.elements[1])

  lines
    .enter()
    .append('line')
    .attr('class', 'normal')
    .attr('transform', d => `translate(${d.p[0]}, ${d.p[1]})`)
    .attr('x2', 0)
    .attr('y2', 0)
    .transition()
    .duration(100)
    .attr('x2', d => d.curvature.elements[0])
    .attr('y2', d => d.curvature.elements[1])

  lines.exit().remove()
}

export default drawNormals

drawTimingsChart.js

const drawTimingsChart = ({ g, x, y, line, car }) => {
  // join
  const paths = g.selectAll('path.car').data([car])

  // enter + update
  paths
    .enter()
    .append('path')
    .attr('class', 'car')
    .merge(paths)
    .attr('d', d => line(d.measures))

  // remove
  paths.exit().remove()

  // join
  const circles = g.selectAll('circle.car').data([car])

  // enter + update
  circles
    .enter()
    .append('circle')
    .attr('class', 'car')
    .attr('r', 5)
    .merge(circles)
    .attr(
      'transform',
      d =>
        `translate(${x(d.measures.slice(-1)[0].pct)} ${y(
          d.measures.slice(-1)[0].speed
        )})`
    )

  // remove
  circles.exit().remove()
}

export default drawTimingsChart

easing.js

const easing = {
  // no easing, no acceleration
  linear: function (t) { return t },
  // accelerating from zero velocity
  easeInQuad: function (t) { return t*t },
  // decelerating to zero velocity
  easeOutQuad: function (t) { return t*(2-t) },
  // acceleration until halfway, then deceleration
  easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
  // accelerating from zero velocity 
  easeInCubic: function (t) { return t*t*t },
  // decelerating to zero velocity 
  easeOutCubic: function (t) { return (--t)*t*t+1 },
  // acceleration until halfway, then deceleration 
  easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
  // accelerating from zero velocity 
  easeInQuart: function (t) { return t*t*t*t },
  // decelerating to zero velocity 
  easeOutQuart: function (t) { return 1-(--t)*t*t*t },
  // acceleration until halfway, then deceleration
  easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
  // accelerating from zero velocity
  easeInQuint: function (t) { return t*t*t*t*t },
  // decelerating to zero velocity
  easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
  // acceleration until halfway, then deceleration 
  easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
}

export default easing

getAngle.js

const getAngle = ([a, b]) => {
  const x = b[0] - a[0]
  const y = -(b[1] - a[1])
  const angle = Math.atan2(y, x) * 180 / Math.PI
  return angle
}

export default getAngle

getCurvature.js

const getCurvature = ({ pct, getCoords, delta = 1 }) => {
  // Get this point.
  const p = getCoords({ pct })

  // Get a point n pixels backward
  // so we can draw a line to it.
  const o = getCoords({ pct: 1 + pct, delta: -delta })

  // Get a point n pixels forward
  // so we can draw a line to it.
  const q = getCoords({ pct, delta })

  // Find po vector.
  const po = $V([o[0] - p[0], o[1] - p[1]])

  // Find pq vector.
  const pq = $V([q[0] - p[0], q[1] - p[1]])

  // Find the angle between the two vectors.
  const angle = po.angleFrom(pq) * 180 / Math.PI

  // Find the normal vector and scale it based on the angle
  // between the two vectors.
  const normal = pq
    .rotate(Math.PI / 2, $V([0, 0]))
    .toUnitVector()
    .multiply(180 - angle)

  // Determine concavity.
  const isConcave = po.angleFrom(normal) * 180 / Math.PI >= 90

  // Create curvature vector.
  const curvature = normal.rotate(isConcave ? 0 : Math.PI, $V([0, 0]))

  return {
    p,
    curvature
  }
}

export default getCurvature

getRandomArbitrary.js

const getRandomArbitrary = (min, max) => Math.random() * (max - min) + min

export default getRandomArbitrary

moveCar.js

import constants from './constants.js'
import getRandomArbitrary from './getRandomArbitrary.js'
import easing from './easing.js'

const moveCar = ({ car, delta, positions }) => {
  const { measures } = car
  const last = measures.slice(-1)[0]

  // First, find the closest upcoming curvature measurement.
  // We will use this to determine whether to slow down,
  // speed up, or maintain current speed.
  const positionIndex = _.findIndex(positions, d => d.pct >= last.pct % 1)

  // This is the curvature strength.
  const modulus = positions[positionIndex].curvature.modulus()

  // If modulus is above a certain threshold,
  let newSpeed = last.speed
  if (modulus > 7) {
    // brake.
    newSpeed = last.speed * 0.87
  } else {
    // Otherwise accelerate.
    newSpeed = last.speed +
      _.clamp(3 * constants.MAX / last.speed, 0, 10)
  }

  // Make sure we're not too slow or too fast.
  newSpeed = _.clamp(newSpeed, constants.MIN, constants.MAX)

  // Calculate new pct.
  const newPct = last.pct + delta * last.speed / 3600000

  return {
    ...car,
    positionIndex,
    measures: [
      ...measures,
      {
        pct: newPct,
        speed: newSpeed
      }
    ]
  }
}

const moveCarOneLap = ({ car, positions }) => {
  let movedCar = {
    ...car,
    measures: [
      {
        pct: 0,
        speed: constants.MAX
      }
    ],
    elapsed: 0
  }

  const delta = 1000 / 60

  let i = 0

  while (movedCar.measures.slice(-1)[0].pct < 1 && i < 10000) {
    movedCar = {
      ...moveCar({ car: movedCar, delta, positions }),
      elapsed: movedCar.elapsed + delta
    }
    i++
  }

  if (movedCar.measures.slice(-1)[0].pct > 1) {
    movedCar = {
      ...movedCar,
      measures: movedCar.measures.slice(0, movedCar.measures.length - 1),
      elapsed: movedCar.elapsed - delta
    }
  }

  // console.log(JSON.stringify(movedCar, null, 2))

  return movedCar
}

export { moveCarOneLap }

export default moveCar

package.json

{
  "standard": {
    "globals": [
      "circuitTracks",
      "d3",
      "$V",
      "_"
    ]
  }
}

style.styl

$black = #010b14
$white = #dacbed
$blue = #0fd0fe
$yellow = #f6df83
$light-blue = darken($blue, 35%)
$red = lighten(#f11c07, 20%)
$outer-padding = 0.5em

$blur-radius = 14px
$text-blur-opacity = 0.75
$svg-blur-opacity = 1
$font-family = 'VT323', monospace

*
  box-sizing border-box
  
html
  background alpha($black, 0.5)
  padding 0
  margin 0
  
body
  background $black
  font-family $font-family
  overflow hidden
  margin 0 auto
  padding $outer-padding
  position relative
  max-width 960px
  max-height 500px
//   // border solid $white 1px

svg
  display block
  
h1
  // visibility hidden
  // border solid red 1px
  color $white
  // text-shadow 0 0 $blur-radius alpha($white, $text-blur-opacity)
  text-transform uppercase
  font-weight normal
  font-style italic
  float left
  text-align center
  padding 0 0 0 0em
  margin 0.5em 0 0 0
  width 25%
  font-size 2em
  span
    text-shadow 0 0 $blur-radius alpha($white, $text-blur-opacity)
    display inline-block
    &.title
      text-shadow 0 0 $blur-radius alpha($red, $text-blur-opacity)
      font-size 1.5em
      color $red
    
.other
  position absolute
  bottom 0.5em
  color $light-blue
    
.timings
  width 75%
  margin-left 25%
  position relative
  
  svg
    // shape-rendering crispEdges
    filter drop-shadow(0 0 $blur-radius alpha($blue, $svg-blur-opacity * 0.5))
      
    path.car
      fill none
      stroke $blue
      
    line.turn
      stroke $light-blue
      stroke-dasharray 1 5
      
    text.turn
      fill $light-blue
      font-size 1.25em
      
    circle.car
      fill $red
      
    .axis
      path
        stroke $light-blue
        stroke-dasharray 1 10
      text
        font-size 1.75em
        font-family $font-family
        fill $light-blue    

.dash
  width 25%
  position absolute
  bottom 5em
  left $outer-padding
  
  svg
    // shape-rendering crispEdges
    filter drop-shadow(0 0 $blur-radius alpha($blue, $svg-blur-opacity * 0.5))

  path.arc
    stroke $white
 
  line.dial
    stroke $red
    stroke-width 2px
    
  circle.dial
    fill $light-blue

  text
    fill $light-blue
    font-size 1.25em
    
    &.label
      font-size 0.9em
    
  line
    stroke $white

.circuit
  width 60%
  // border solid blue 1px
  float left
  // height 500px
  position relative
  margin 0 0 0 7.5%
  padding 0
  
  h1
    position absolute
    font-size 1.25em
    width 100%
    text-align center
    top 7em
    color $light-blue
    text-shadow 0 0 $blur-radius alpha($light-blue, $text-blur-opacity)

  svg
    // margin 0 auto
    filter drop-shadow(0 0 $blur-radius alpha($white, $svg-blur-opacity))
    // shape-rendering crispEdges
    overflow visible
      
    path.track
      fill none
      stroke $blue
      opacity 0.2
            
    path.car
      fill $red

    circle
      fill $red
      &.turn
        fill $red
        
    line
      &.normal
        stroke $white
      &.finish-line
        stroke $white
        stroke-width 2px
      
    text
      fill $white
      
      &.turn
        fill $light-blue
        font-size 1.25em
      

sylvester.js

eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('9 17={3i:\'0.1.3\',16:1e-6};l v(){}v.23={e:l(i){8(i<1||i>7.4.q)?w:7.4[i-1]},2R:l(){8 7.4.q},1u:l(){8 F.1x(7.2u(7))},24:l(a){9 n=7.4.q;9 V=a.4||a;o(n!=V.q){8 1L}J{o(F.13(7.4[n-1]-V[n-1])>17.16){8 1L}}H(--n);8 2x},1q:l(){8 v.u(7.4)},1b:l(a){9 b=[];7.28(l(x,i){b.19(a(x,i))});8 v.u(b)},28:l(a){9 n=7.4.q,k=n,i;J{i=k-n;a(7.4[i],i+1)}H(--n)},2q:l(){9 r=7.1u();o(r===0){8 7.1q()}8 7.1b(l(x){8 x/r})},1C:l(a){9 V=a.4||a;9 n=7.4.q,k=n,i;o(n!=V.q){8 w}9 b=0,1D=0,1F=0;7.28(l(x,i){b+=x*V[i-1];1D+=x*x;1F+=V[i-1]*V[i-1]});1D=F.1x(1D);1F=F.1x(1F);o(1D*1F===0){8 w}9 c=b/(1D*1F);o(c<-1){c=-1}o(c>1){c=1}8 F.37(c)},1m:l(a){9 b=7.1C(a);8(b===w)?w:(b<=17.16)},34:l(a){9 b=7.1C(a);8(b===w)?w:(F.13(b-F.1A)<=17.16)},2k:l(a){9 b=7.2u(a);8(b===w)?w:(F.13(b)<=17.16)},2j:l(a){9 V=a.4||a;o(7.4.q!=V.q){8 w}8 7.1b(l(x,i){8 x+V[i-1]})},2C:l(a){9 V=a.4||a;o(7.4.q!=V.q){8 w}8 7.1b(l(x,i){8 x-V[i-1]})},22:l(k){8 7.1b(l(x){8 x*k})},x:l(k){8 7.22(k)},2u:l(a){9 V=a.4||a;9 i,2g=0,n=7.4.q;o(n!=V.q){8 w}J{2g+=7.4[n-1]*V[n-1]}H(--n);8 2g},2f:l(a){9 B=a.4||a;o(7.4.q!=3||B.q!=3){8 w}9 A=7.4;8 v.u([(A[1]*B[2])-(A[2]*B[1]),(A[2]*B[0])-(A[0]*B[2]),(A[0]*B[1])-(A[1]*B[0])])},2A:l(){9 m=0,n=7.4.q,k=n,i;J{i=k-n;o(F.13(7.4[i])>F.13(m)){m=7.4[i]}}H(--n);8 m},2Z:l(x){9 a=w,n=7.4.q,k=n,i;J{i=k-n;o(a===w&&7.4[i]==x){a=i+1}}H(--n);8 a},3g:l(){8 S.2X(7.4)},2d:l(){8 7.1b(l(x){8 F.2d(x)})},2V:l(x){8 7.1b(l(y){8(F.13(y-x)<=17.16)?x:y})},1o:l(a){o(a.K){8 a.1o(7)}9 V=a.4||a;o(V.q!=7.4.q){8 w}9 b=0,2b;7.28(l(x,i){2b=x-V[i-1];b+=2b*2b});8 F.1x(b)},3a:l(a){8 a.1h(7)},2T:l(a){8 a.1h(7)},1V:l(t,a){9 V,R,x,y,z;2S(7.4.q){27 2:V=a.4||a;o(V.q!=2){8 w}R=S.1R(t).4;x=7.4[0]-V[0];y=7.4[1]-V[1];8 v.u([V[0]+R[0][0]*x+R[0][1]*y,V[1]+R[1][0]*x+R[1][1]*y]);1I;27 3:o(!a.U){8 w}9 C=a.1r(7).4;R=S.1R(t,a.U).4;x=7.4[0]-C[0];y=7.4[1]-C[1];z=7.4[2]-C[2];8 v.u([C[0]+R[0][0]*x+R[0][1]*y+R[0][2]*z,C[1]+R[1][0]*x+R[1][1]*y+R[1][2]*z,C[2]+R[2][0]*x+R[2][1]*y+R[2][2]*z]);1I;2P:8 w}},1t:l(a){o(a.K){9 P=7.4.2O();9 C=a.1r(P).4;8 v.u([C[0]+(C[0]-P[0]),C[1]+(C[1]-P[1]),C[2]+(C[2]-(P[2]||0))])}1d{9 Q=a.4||a;o(7.4.q!=Q.q){8 w}8 7.1b(l(x,i){8 Q[i-1]+(Q[i-1]-x)})}},1N:l(){9 V=7.1q();2S(V.4.q){27 3:1I;27 2:V.4.19(0);1I;2P:8 w}8 V},2n:l(){8\'[\'+7.4.2K(\', \')+\']\'},26:l(a){7.4=(a.4||a).2O();8 7}};v.u=l(a){9 V=25 v();8 V.26(a)};v.i=v.u([1,0,0]);v.j=v.u([0,1,0]);v.k=v.u([0,0,1]);v.2J=l(n){9 a=[];J{a.19(F.2F())}H(--n);8 v.u(a)};v.1j=l(n){9 a=[];J{a.19(0)}H(--n);8 v.u(a)};l S(){}S.23={e:l(i,j){o(i<1||i>7.4.q||j<1||j>7.4[0].q){8 w}8 7.4[i-1][j-1]},33:l(i){o(i>7.4.q){8 w}8 v.u(7.4[i-1])},2E:l(j){o(j>7.4[0].q){8 w}9 a=[],n=7.4.q,k=n,i;J{i=k-n;a.19(7.4[i][j-1])}H(--n);8 v.u(a)},2R:l(){8{2D:7.4.q,1p:7.4[0].q}},2D:l(){8 7.4.q},1p:l(){8 7.4[0].q},24:l(a){9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}o(7.4.q!=M.q||7.4[0].q!=M[0].q){8 1L}9 b=7.4.q,15=b,i,G,10=7.4[0].q,j;J{i=15-b;G=10;J{j=10-G;o(F.13(7.4[i][j]-M[i][j])>17.16){8 1L}}H(--G)}H(--b);8 2x},1q:l(){8 S.u(7.4)},1b:l(a){9 b=[],12=7.4.q,15=12,i,G,10=7.4[0].q,j;J{i=15-12;G=10;b[i]=[];J{j=10-G;b[i][j]=a(7.4[i][j],i+1,j+1)}H(--G)}H(--12);8 S.u(b)},2i:l(a){9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}8(7.4.q==M.q&&7.4[0].q==M[0].q)},2j:l(a){9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}o(!7.2i(M)){8 w}8 7.1b(l(x,i,j){8 x+M[i-1][j-1]})},2C:l(a){9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}o(!7.2i(M)){8 w}8 7.1b(l(x,i,j){8 x-M[i-1][j-1]})},2B:l(a){9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}8(7.4[0].q==M.q)},22:l(a){o(!a.4){8 7.1b(l(x){8 x*a})}9 b=a.1u?2x:1L;9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}o(!7.2B(M)){8 w}9 d=7.4.q,15=d,i,G,10=M[0].q,j;9 e=7.4[0].q,4=[],21,20,c;J{i=15-d;4[i]=[];G=10;J{j=10-G;21=0;20=e;J{c=e-20;21+=7.4[i][c]*M[c][j]}H(--20);4[i][j]=21}H(--G)}H(--d);9 M=S.u(4);8 b?M.2E(1):M},x:l(a){8 7.22(a)},32:l(a,b,c,d){9 e=[],12=c,i,G,j;9 f=7.4.q,1p=7.4[0].q;J{i=c-12;e[i]=[];G=d;J{j=d-G;e[i][j]=7.4[(a+i-1)%f][(b+j-1)%1p]}H(--G)}H(--12);8 S.u(e)},31:l(){9 a=7.4.q,1p=7.4[0].q;9 b=[],12=1p,i,G,j;J{i=1p-12;b[i]=[];G=a;J{j=a-G;b[i][j]=7.4[j][i]}H(--G)}H(--12);8 S.u(b)},1y:l(){8(7.4.q==7.4[0].q)},2A:l(){9 m=0,12=7.4.q,15=12,i,G,10=7.4[0].q,j;J{i=15-12;G=10;J{j=10-G;o(F.13(7.4[i][j])>F.13(m)){m=7.4[i][j]}}H(--G)}H(--12);8 m},2Z:l(x){9 a=w,12=7.4.q,15=12,i,G,10=7.4[0].q,j;J{i=15-12;G=10;J{j=10-G;o(7.4[i][j]==x){8{i:i+1,j:j+1}}}H(--G)}H(--12);8 w},30:l(){o(!7.1y){8 w}9 a=[],n=7.4.q,k=n,i;J{i=k-n;a.19(7.4[i][i])}H(--n);8 v.u(a)},1K:l(){9 M=7.1q(),1c;9 n=7.4.q,k=n,i,1s,1n=7.4[0].q,p;J{i=k-n;o(M.4[i][i]==0){2e(j=i+1;j<k;j++){o(M.4[j][i]!=0){1c=[];1s=1n;J{p=1n-1s;1c.19(M.4[i][p]+M.4[j][p])}H(--1s);M.4[i]=1c;1I}}}o(M.4[i][i]!=0){2e(j=i+1;j<k;j++){9 a=M.4[j][i]/M.4[i][i];1c=[];1s=1n;J{p=1n-1s;1c.19(p<=i?0:M.4[j][p]-M.4[i][p]*a)}H(--1s);M.4[j]=1c}}}H(--n);8 M},3h:l(){8 7.1K()},2z:l(){o(!7.1y()){8 w}9 M=7.1K();9 a=M.4[0][0],n=M.4.q-1,k=n,i;J{i=k-n+1;a=a*M.4[i][i]}H(--n);8 a},3f:l(){8 7.2z()},2y:l(){8(7.1y()&&7.2z()===0)},2Y:l(){o(!7.1y()){8 w}9 a=7.4[0][0],n=7.4.q-1,k=n,i;J{i=k-n+1;a+=7.4[i][i]}H(--n);8 a},3e:l(){8 7.2Y()},1Y:l(){9 M=7.1K(),1Y=0;9 a=7.4.q,15=a,i,G,10=7.4[0].q,j;J{i=15-a;G=10;J{j=10-G;o(F.13(M.4[i][j])>17.16){1Y++;1I}}H(--G)}H(--a);8 1Y},3d:l(){8 7.1Y()},2W:l(a){9 M=a.4||a;o(1g(M[0][0])==\'1f\'){M=S.u(M).4}9 T=7.1q(),1p=T.4[0].q;9 b=T.4.q,15=b,i,G,10=M[0].q,j;o(b!=M.q){8 w}J{i=15-b;G=10;J{j=10-G;T.4[i][1p+j]=M[i][j]}H(--G)}H(--b);8 T},2w:l(){o(!7.1y()||7.2y()){8 w}9 a=7.4.q,15=a,i,j;9 M=7.2W(S.I(a)).1K();9 b,1n=M.4[0].q,p,1c,2v;9 c=[],2c;J{i=a-1;1c=[];b=1n;c[i]=[];2v=M.4[i][i];J{p=1n-b;2c=M.4[i][p]/2v;1c.19(2c);o(p>=15){c[i].19(2c)}}H(--b);M.4[i]=1c;2e(j=0;j<i;j++){1c=[];b=1n;J{p=1n-b;1c.19(M.4[j][p]-M.4[i][p]*M.4[j][i])}H(--b);M.4[j]=1c}}H(--a);8 S.u(c)},3c:l(){8 7.2w()},2d:l(){8 7.1b(l(x){8 F.2d(x)})},2V:l(x){8 7.1b(l(p){8(F.13(p-x)<=17.16)?x:p})},2n:l(){9 a=[];9 n=7.4.q,k=n,i;J{i=k-n;a.19(v.u(7.4[i]).2n())}H(--n);8 a.2K(\'\\n\')},26:l(a){9 i,4=a.4||a;o(1g(4[0][0])!=\'1f\'){9 b=4.q,15=b,G,10,j;7.4=[];J{i=15-b;G=4[i].q;10=G;7.4[i]=[];J{j=10-G;7.4[i][j]=4[i][j]}H(--G)}H(--b);8 7}9 n=4.q,k=n;7.4=[];J{i=k-n;7.4.19([4[i]])}H(--n);8 7}};S.u=l(a){9 M=25 S();8 M.26(a)};S.I=l(n){9 a=[],k=n,i,G,j;J{i=k-n;a[i]=[];G=k;J{j=k-G;a[i][j]=(i==j)?1:0}H(--G)}H(--n);8 S.u(a)};S.2X=l(a){9 n=a.q,k=n,i;9 M=S.I(n);J{i=k-n;M.4[i][i]=a[i]}H(--n);8 M};S.1R=l(b,a){o(!a){8 S.u([[F.1H(b),-F.1G(b)],[F.1G(b),F.1H(b)]])}9 d=a.1q();o(d.4.q!=3){8 w}9 e=d.1u();9 x=d.4[0]/e,y=d.4[1]/e,z=d.4[2]/e;9 s=F.1G(b),c=F.1H(b),t=1-c;8 S.u([[t*x*x+c,t*x*y-s*z,t*x*z+s*y],[t*x*y+s*z,t*y*y+c,t*y*z-s*x],[t*x*z-s*y,t*y*z+s*x,t*z*z+c]])};S.3b=l(t){9 c=F.1H(t),s=F.1G(t);8 S.u([[1,0,0],[0,c,-s],[0,s,c]])};S.39=l(t){9 c=F.1H(t),s=F.1G(t);8 S.u([[c,0,s],[0,1,0],[-s,0,c]])};S.38=l(t){9 c=F.1H(t),s=F.1G(t);8 S.u([[c,-s,0],[s,c,0],[0,0,1]])};S.2J=l(n,m){8 S.1j(n,m).1b(l(){8 F.2F()})};S.1j=l(n,m){9 a=[],12=n,i,G,j;J{i=n-12;a[i]=[];G=m;J{j=m-G;a[i][j]=0}H(--G)}H(--12);8 S.u(a)};l 14(){}14.23={24:l(a){8(7.1m(a)&&7.1h(a.K))},1q:l(){8 14.u(7.K,7.U)},2U:l(a){9 V=a.4||a;8 14.u([7.K.4[0]+V[0],7.K.4[1]+V[1],7.K.4[2]+(V[2]||0)],7.U)},1m:l(a){o(a.W){8 a.1m(7)}9 b=7.U.1C(a.U);8(F.13(b)<=17.16||F.13(b-F.1A)<=17.16)},1o:l(a){o(a.W){8 a.1o(7)}o(a.U){o(7.1m(a)){8 7.1o(a.K)}9 N=7.U.2f(a.U).2q().4;9 A=7.K.4,B=a.K.4;8 F.13((A[0]-B[0])*N[0]+(A[1]-B[1])*N[1]+(A[2]-B[2])*N[2])}1d{9 P=a.4||a;9 A=7.K.4,D=7.U.4;9 b=P[0]-A[0],2a=P[1]-A[1],29=(P[2]||0)-A[2];9 c=F.1x(b*b+2a*2a+29*29);o(c===0)8 0;9 d=(b*D[0]+2a*D[1]+29*D[2])/c;9 e=1-d*d;8 F.13(c*F.1x(e<0?0:e))}},1h:l(a){9 b=7.1o(a);8(b!==w&&b<=17.16)},2T:l(a){8 a.1h(7)},1v:l(a){o(a.W){8 a.1v(7)}8(!7.1m(a)&&7.1o(a)<=17.16)},1U:l(a){o(a.W){8 a.1U(7)}o(!7.1v(a)){8 w}9 P=7.K.4,X=7.U.4,Q=a.K.4,Y=a.U.4;9 b=X[0],1z=X[1],1B=X[2],1T=Y[0],1S=Y[1],1M=Y[2];9 c=P[0]-Q[0],2s=P[1]-Q[1],2r=P[2]-Q[2];9 d=-b*c-1z*2s-1B*2r;9 e=1T*c+1S*2s+1M*2r;9 f=b*b+1z*1z+1B*1B;9 g=1T*1T+1S*1S+1M*1M;9 h=b*1T+1z*1S+1B*1M;9 k=(d*g/f+h*e)/(g-h*h);8 v.u([P[0]+k*b,P[1]+k*1z,P[2]+k*1B])},1r:l(a){o(a.U){o(7.1v(a)){8 7.1U(a)}o(7.1m(a)){8 w}9 D=7.U.4,E=a.U.4;9 b=D[0],1l=D[1],1k=D[2],1P=E[0],1O=E[1],1Q=E[2];9 x=(1k*1P-b*1Q),y=(b*1O-1l*1P),z=(1l*1Q-1k*1O);9 N=v.u([x*1Q-y*1O,y*1P-z*1Q,z*1O-x*1P]);9 P=11.u(a.K,N);8 P.1U(7)}1d{9 P=a.4||a;o(7.1h(P)){8 v.u(P)}9 A=7.K.4,D=7.U.4;9 b=D[0],1l=D[1],1k=D[2],1w=A[0],18=A[1],1a=A[2];9 x=b*(P[1]-18)-1l*(P[0]-1w),y=1l*((P[2]||0)-1a)-1k*(P[1]-18),z=1k*(P[0]-1w)-b*((P[2]||0)-1a);9 V=v.u([1l*x-1k*z,1k*y-b*x,b*z-1l*y]);9 k=7.1o(P)/V.1u();8 v.u([P[0]+V.4[0]*k,P[1]+V.4[1]*k,(P[2]||0)+V.4[2]*k])}},1V:l(t,a){o(1g(a.U)==\'1f\'){a=14.u(a.1N(),v.k)}9 R=S.1R(t,a.U).4;9 C=a.1r(7.K).4;9 A=7.K.4,D=7.U.4;9 b=C[0],1E=C[1],1J=C[2],1w=A[0],18=A[1],1a=A[2];9 x=1w-b,y=18-1E,z=1a-1J;8 14.u([b+R[0][0]*x+R[0][1]*y+R[0][2]*z,1E+R[1][0]*x+R[1][1]*y+R[1][2]*z,1J+R[2][0]*x+R[2][1]*y+R[2][2]*z],[R[0][0]*D[0]+R[0][1]*D[1]+R[0][2]*D[2],R[1][0]*D[0]+R[1][1]*D[1]+R[1][2]*D[2],R[2][0]*D[0]+R[2][1]*D[1]+R[2][2]*D[2]])},1t:l(a){o(a.W){9 A=7.K.4,D=7.U.4;9 b=A[0],18=A[1],1a=A[2],2N=D[0],1l=D[1],1k=D[2];9 c=7.K.1t(a).4;9 d=b+2N,2h=18+1l,2o=1a+1k;9 Q=a.1r([d,2h,2o]).4;9 e=[Q[0]+(Q[0]-d)-c[0],Q[1]+(Q[1]-2h)-c[1],Q[2]+(Q[2]-2o)-c[2]];8 14.u(c,e)}1d o(a.U){8 7.1V(F.1A,a)}1d{9 P=a.4||a;8 14.u(7.K.1t([P[0],P[1],(P[2]||0)]),7.U)}},1Z:l(a,b){a=v.u(a);b=v.u(b);o(a.4.q==2){a.4.19(0)}o(b.4.q==2){b.4.19(0)}o(a.4.q>3||b.4.q>3){8 w}9 c=b.1u();o(c===0){8 w}7.K=a;7.U=v.u([b.4[0]/c,b.4[1]/c,b.4[2]/c]);8 7}};14.u=l(a,b){9 L=25 14();8 L.1Z(a,b)};14.X=14.u(v.1j(3),v.i);14.Y=14.u(v.1j(3),v.j);14.Z=14.u(v.1j(3),v.k);l 11(){}11.23={24:l(a){8(7.1h(a.K)&&7.1m(a))},1q:l(){8 11.u(7.K,7.W)},2U:l(a){9 V=a.4||a;8 11.u([7.K.4[0]+V[0],7.K.4[1]+V[1],7.K.4[2]+(V[2]||0)],7.W)},1m:l(a){9 b;o(a.W){b=7.W.1C(a.W);8(F.13(b)<=17.16||F.13(F.1A-b)<=17.16)}1d o(a.U){8 7.W.2k(a.U)}8 w},2k:l(a){9 b=7.W.1C(a.W);8(F.13(F.1A/2-b)<=17.16)},1o:l(a){o(7.1v(a)||7.1h(a)){8 0}o(a.K){9 A=7.K.4,B=a.K.4,N=7.W.4;8 F.13((A[0]-B[0])*N[0]+(A[1]-B[1])*N[1]+(A[2]-B[2])*N[2])}1d{9 P=a.4||a;9 A=7.K.4,N=7.W.4;8 F.13((A[0]-P[0])*N[0]+(A[1]-P[1])*N[1]+(A[2]-(P[2]||0))*N[2])}},1h:l(a){o(a.W){8 w}o(a.U){8(7.1h(a.K)&&7.1h(a.K.2j(a.U)))}1d{9 P=a.4||a;9 A=7.K.4,N=7.W.4;9 b=F.13(N[0]*(A[0]-P[0])+N[1]*(A[1]-P[1])+N[2]*(A[2]-(P[2]||0)));8(b<=17.16)}},1v:l(a){o(1g(a.U)==\'1f\'&&1g(a.W)==\'1f\'){8 w}8!7.1m(a)},1U:l(a){o(!7.1v(a)){8 w}o(a.U){9 A=a.K.4,D=a.U.4,P=7.K.4,N=7.W.4;9 b=(N[0]*(P[0]-A[0])+N[1]*(P[1]-A[1])+N[2]*(P[2]-A[2]))/(N[0]*D[0]+N[1]*D[1]+N[2]*D[2]);8 v.u([A[0]+D[0]*b,A[1]+D[1]*b,A[2]+D[2]*b])}1d o(a.W){9 c=7.W.2f(a.W).2q();9 N=7.W.4,A=7.K.4,O=a.W.4,B=a.K.4;9 d=S.1j(2,2),i=0;H(d.2y()){i++;d=S.u([[N[i%3],N[(i+1)%3]],[O[i%3],O[(i+1)%3]]])}9 e=d.2w().4;9 x=N[0]*A[0]+N[1]*A[1]+N[2]*A[2];9 y=O[0]*B[0]+O[1]*B[1]+O[2]*B[2];9 f=[e[0][0]*x+e[0][1]*y,e[1][0]*x+e[1][1]*y];9 g=[];2e(9 j=1;j<=3;j++){g.19((i==j)?0:f[(j+(5-i)%3)%3])}8 14.u(g,c)}},1r:l(a){9 P=a.4||a;9 A=7.K.4,N=7.W.4;9 b=(A[0]-P[0])*N[0]+(A[1]-P[1])*N[1]+(A[2]-(P[2]||0))*N[2];8 v.u([P[0]+N[0]*b,P[1]+N[1]*b,(P[2]||0)+N[2]*b])},1V:l(t,a){9 R=S.1R(t,a.U).4;9 C=a.1r(7.K).4;9 A=7.K.4,N=7.W.4;9 b=C[0],1E=C[1],1J=C[2],1w=A[0],18=A[1],1a=A[2];9 x=1w-b,y=18-1E,z=1a-1J;8 11.u([b+R[0][0]*x+R[0][1]*y+R[0][2]*z,1E+R[1][0]*x+R[1][1]*y+R[1][2]*z,1J+R[2][0]*x+R[2][1]*y+R[2][2]*z],[R[0][0]*N[0]+R[0][1]*N[1]+R[0][2]*N[2],R[1][0]*N[0]+R[1][1]*N[1]+R[1][2]*N[2],R[2][0]*N[0]+R[2][1]*N[1]+R[2][2]*N[2]])},1t:l(a){o(a.W){9 A=7.K.4,N=7.W.4;9 b=A[0],18=A[1],1a=A[2],2M=N[0],2L=N[1],2Q=N[2];9 c=7.K.1t(a).4;9 d=b+2M,2p=18+2L,2m=1a+2Q;9 Q=a.1r([d,2p,2m]).4;9 e=[Q[0]+(Q[0]-d)-c[0],Q[1]+(Q[1]-2p)-c[1],Q[2]+(Q[2]-2m)-c[2]];8 11.u(c,e)}1d o(a.U){8 7.1V(F.1A,a)}1d{9 P=a.4||a;8 11.u(7.K.1t([P[0],P[1],(P[2]||0)]),7.W)}},1Z:l(a,b,c){a=v.u(a);a=a.1N();o(a===w){8 w}b=v.u(b);b=b.1N();o(b===w){8 w}o(1g(c)==\'1f\'){c=w}1d{c=v.u(c);c=c.1N();o(c===w){8 w}}9 d=a.4[0],18=a.4[1],1a=a.4[2];9 e=b.4[0],1W=b.4[1],1X=b.4[2];9 f,1i;o(c!==w){9 g=c.4[0],2l=c.4[1],2t=c.4[2];f=v.u([(1W-18)*(2t-1a)-(1X-1a)*(2l-18),(1X-1a)*(g-d)-(e-d)*(2t-1a),(e-d)*(2l-18)-(1W-18)*(g-d)]);1i=f.1u();o(1i===0){8 w}f=v.u([f.4[0]/1i,f.4[1]/1i,f.4[2]/1i])}1d{1i=F.1x(e*e+1W*1W+1X*1X);o(1i===0){8 w}f=v.u([b.4[0]/1i,b.4[1]/1i,b.4[2]/1i])}7.K=a;7.W=f;8 7}};11.u=l(a,b,c){9 P=25 11();8 P.1Z(a,b,c)};11.2I=11.u(v.1j(3),v.k);11.2H=11.u(v.1j(3),v.i);11.2G=11.u(v.1j(3),v.j);11.36=11.2I;11.35=11.2H;11.3j=11.2G;9 $V=v.u;9 $M=S.u;9 $L=14.u;9 $P=11.u;',62,206,'||||elements|||this|return|var||||||||||||function|||if||length||||create|Vector|null|||||||||Math|nj|while||do|anchor||||||||Matrix||direction||normal||||kj|Plane|ni|abs|Line|ki|precision|Sylvester|A2|push|A3|map|els|else||undefined|typeof|contains|mod|Zero|D3|D2|isParallelTo|kp|distanceFrom|cols|dup|pointClosestTo|np|reflectionIn|modulus|intersects|A1|sqrt|isSquare|X2|PI|X3|angleFrom|mod1|C2|mod2|sin|cos|break|C3|toRightTriangular|false|Y3|to3D|E2|E1|E3|Rotation|Y2|Y1|intersectionWith|rotate|v12|v13|rank|setVectors|nc|sum|multiply|prototype|eql|new|setElements|case|each|PA3|PA2|part|new_element|round|for|cross|product|AD2|isSameSizeAs|add|isPerpendicularTo|v22|AN3|inspect|AD3|AN2|toUnitVector|PsubQ3|PsubQ2|v23|dot|divisor|inverse|true|isSingular|determinant|max|canMultiplyFromLeft|subtract|rows|col|random|ZX|YZ|XY|Random|join|N2|N1|D1|slice|default|N3|dimensions|switch|liesIn|translate|snapTo|augment|Diagonal|trace|indexOf|diagonal|transpose|minor|row|isAntiparallelTo|ZY|YX|acos|RotationZ|RotationY|liesOn|RotationX|inv|rk|tr|det|toDiagonalMatrix|toUpperTriangular|version|XZ'.split('|'),0,{}))