streetmix/streetmix

View on GitHub
assets/scripts/streets/thumbnail.js

Summary

Maintainability
D
2 days
Test Coverage
import { images } from '../app/load_resources'
import { drawLine } from '../util/canvas_drawing'
import { prettifyWidth } from '../util/width_units'
import { getSkyboxDef, makeCanvasGradientStopArray } from '../sky'
import {
  BUILDINGS,
  GROUND_BASELINE_HEIGHT,
  drawBuilding
} from '../segments/buildings'
import { getSegmentInfo, getSegmentVariantInfo } from '../segments/info'
import { TILE_SIZE } from '../segments/constants'
import {
  getVariantInfoDimensions,
  drawSegmentContents,
  getLocaleSegmentName
} from '../segments/view'
import { formatMessage } from '../locales/locale'
import { SAVE_AS_IMAGE_NAMES_WIDTHS_PADDING } from './image'

const BOTTOM_BACKGROUND = 'rgb(216, 211, 203)'
const BACKGROUND_DIRT_COLOUR = 'rgb(53, 45, 39)'
const SILHOUETTE_FILL_COLOUR = 'rgb(240, 240, 240)'

const SEGMENT_NAME_FONT = 'Rubik'
const SEGMENT_NAME_FONT_SIZE = 12
const SEGMENT_NAME_FONT_WEIGHT = '400'

const STREET_NAME_FONT = 'overpass'
const STREET_NAME_FONT_SIZE = 70
const STREET_NAME_FONT_WEIGHT = '700'

const WATERMARK_FONT = 'Rubik'
const WATERMARK_FONT_SIZE = 24
const WATERMARK_FONT_WEIGHT = '600'
const WATERMARK_RIGHT_MARGIN = 15
const WATERMARK_BOTTOM_MARGIN = 15
const WATERMARK_DARK_COLOR = '#333333'
const WATERMARK_LIGHT_COLOR = '#cccccc'

const WORDMARK_MARGIN = 4

/**
 * Draws sky.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Object} street - street data
 * @param {Number} width - width of area to draw
 * @param {Number} height - width of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Number} multiplier - scale factor of image
 * @param {Number} horizonLine - vertical height of horizon
 * @param {Number} groundLevel - vertical height of ground
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawSky (
  ctx,
  street,
  width,
  height,
  dpi,
  multiplier,
  horizonLine,
  groundLevel
) {
  const sky = getSkyboxDef(street.skybox)

  // Solid color fill
  if (sky.backgroundColor) {
    drawBackgroundColor(ctx, width, horizonLine, dpi, sky.backgroundColor)
  }

  // Background image fill
  if (sky.backgroundImage) {
    drawBackgroundImage(
      ctx,
      width,
      height,
      dpi,
      multiplier,
      sky.backgroundImage
    )
  }

  // Gradient fill
  if (sky.backgroundGradient) {
    drawBackgroundGradient(ctx, width, horizonLine, dpi, sky.backgroundGradient)
  }

  // Background objects
  if (sky.backgroundObjects) {
    drawBackgroundObjects(
      ctx,
      width,
      height,
      dpi,
      multiplier,
      sky.backgroundObjects
    )
  }

  // Clouds
  drawClouds(ctx, width, groundLevel, dpi, sky)
}

/**
 * Draws a layer of background color
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {string} color - color to render
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawBackgroundColor (ctx, width, height, dpi, color) {
  ctx.fillStyle = color
  ctx.fillRect(0, 0, width * dpi, height * dpi)
}

/**
 * Draws background image as a repeating pattern.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Object} imageId - image ID to render
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawBackgroundImage (ctx, width, height, dpi, multiplier, imageId) {
  const img = images.get(imageId)

  for (let i = 0; i < Math.floor((height / img.height) * multiplier) + 1; i++) {
    for (let j = 0; j < Math.floor((width / img.width) * multiplier) + 1; j++) {
      ctx.drawImage(
        img.img,
        0,
        0,
        img.width,
        img.height,
        j * img.width * dpi * multiplier,
        i * img.height * dpi * multiplier,
        img.width * dpi * multiplier,
        img.height * dpi * multiplier
      )
    }
  }
}

/**
 * Draws background linear gradient.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Array} backgroundGradient - skybox definition of gradient
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawBackgroundGradient (ctx, width, height, dpi, backgroundGradient) {
  const gradient = ctx.createLinearGradient(0, 0, 0, height * dpi)

  // Make color stops
  const stops = makeCanvasGradientStopArray(backgroundGradient)
  for (let i = 0; i < stops.length; i++) {
    const [color, stop] = stops[i]
    gradient.addColorStop(stop, color)
  }

  ctx.fillStyle = gradient
  ctx.fillRect(0, 0, width * dpi, height * dpi)
}

/**
 * Draws background linear gradient.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - scale factor of image
 * @param {Array} objects - skybox definition of background objects
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawBackgroundObjects (ctx, width, height, dpi, multiplier, objects) {
  objects.forEach((object) => {
    const {
      image: imageId,
      width: imageWidth,
      height: imageHeight,
      top,
      left
    } = object
    const image = images.get(imageId).img
    ctx.drawImage(
      image,
      // Left and top values are "percentage" values
      // and sets where the center of the image is
      (left * width - imageWidth / 2) * dpi * multiplier,
      (top * height - imageHeight / 2) * dpi * multiplier,
      imageWidth * dpi * multiplier,
      imageHeight * dpi * multiplier
    )
  })
}

/**
 * Draws clouds.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Object} sky - skybox settings
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawClouds (ctx, width, height, dpi, sky) {
  // Handle cloud opacity
  ctx.save()
  ctx.globalAlpha = sky.cloudOpacity ?? 1

  // Grab images
  const skyFrontImg = images.get('/images/sky-front.svg')
  const skyRearImg = images.get('/images/sky-rear.svg')

  // Source images are 2x what they need to be for the math to work
  // so until we resize the intrinsic size of the images, we have to
  // do this and then size it back up later
  const skyFrontWidth = skyFrontImg.width / 2
  const skyFrontHeight = skyFrontImg.height / 2
  const skyRearWidth = skyRearImg.width / 2
  const skyRearHeight = skyRearImg.height / 2

  // TODO document magic numbers
  // y1 = top edge of sky-front image
  const y1 = height - skyFrontHeight

  for (let i = 0; i < Math.floor(width / skyFrontWidth) + 1; i++) {
    ctx.drawImage(
      skyFrontImg.img,
      0,
      0,
      skyFrontWidth * 2,
      skyFrontHeight * 2, // todo: change intrinsic size
      i * skyFrontWidth * dpi,
      y1 * dpi,
      skyFrontWidth * dpi,
      skyFrontHeight * dpi
    )
  }

  // TODO document magic numbers
  // y2 = top edge of sky-rear is 120 pixels above the top edge of sky-front
  const y2 = height - skyFrontHeight - 120

  for (let i = 0; i < Math.floor(width / skyRearWidth) + 1; i++) {
    ctx.drawImage(
      skyRearImg.img,
      0,
      0,
      skyRearWidth * 2,
      skyRearHeight * 2, // todo: change intrinsic size
      i * skyRearWidth * dpi,
      y2 * dpi,
      skyRearWidth * dpi,
      skyRearHeight * dpi
    )
  }

  // Restore global opacity
  ctx.restore()
}

/**
 * Draws ground.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Object} street - street data
 * @param {Number} width - width of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Number} multiplier - scale factor of image
 * @param {Number} horizonLine - vertical height of horizon
 * @param {Number} groundLevel - vertical height of ground
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawGround (
  ctx,
  street,
  width,
  dpi,
  multiplier,
  horizonLine,
  groundLevel
) {
  ctx.fillStyle = BACKGROUND_DIRT_COLOUR
  ctx.fillRect(0, horizonLine * dpi, width * dpi, 25 * multiplier * dpi)

  ctx.fillRect(
    0,
    groundLevel * dpi,
    (width / 2 - (street.width * TILE_SIZE * multiplier) / 2) * dpi,
    20 * multiplier * dpi
  )

  ctx.fillRect(
    (width / 2 + (street.width * TILE_SIZE * multiplier) / 2) * dpi,
    groundLevel * dpi,
    width * dpi,
    20 * multiplier * dpi
  )
}

/**
 * Draws buildings.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Object} street - street data
 * @param {Number} width - width of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Number} multiplier - scale factor of image
 * @param {Number} groundLevel - vertical height of ground
 * @param {Number} buildingOffsetLeft
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawBuildings (
  ctx,
  street,
  width,
  dpi,
  multiplier,
  groundLevel,
  buildingOffsetLeft
) {
  const buildingWidth = buildingOffsetLeft / multiplier

  // Left building
  const x1 = width / 2 - (street.width * TILE_SIZE * multiplier) / 2
  const leftBuilding = BUILDINGS[street.leftBuildingVariant]
  const leftOverhang =
    typeof leftBuilding.overhangWidth === 'number'
      ? leftBuilding.overhangWidth
      : 0
  drawBuilding(
    ctx,
    street.leftBuildingVariant,
    street.leftBuildingHeight,
    'left',
    buildingWidth,
    groundLevel,
    x1 - (buildingWidth - leftOverhang) * multiplier,
    multiplier,
    dpi
  )

  // Right building
  const x2 = width / 2 + (street.width * TILE_SIZE * multiplier) / 2
  const rightBuilding = BUILDINGS[street.rightBuildingVariant]
  const rightOverhang =
    typeof rightBuilding.overhangWidth === 'number'
      ? rightBuilding.overhangWidth
      : 0
  drawBuilding(
    ctx,
    street.rightBuildingVariant,
    street.rightBuildingHeight,
    'right',
    buildingWidth,
    groundLevel,
    x2 - rightOverhang * multiplier,
    multiplier,
    dpi
  )
}

/**
 * Draws segments.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Object} street - street data
 * @param {Number} dpi - pixel density of canvas
 * @param {Number} multiplier - scale factor of image
 * @param {Number} groundLevel - vertical height of ground
 * @param {Number} offsetLeft - left position to start from
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawSegments (ctx, street, dpi, multiplier, groundLevel, offsetLeft) {
  // Collect z-indexes
  const zIndexes = []
  for (const segment of street.segments) {
    const segmentInfo = getSegmentInfo(segment.type)

    if (zIndexes.indexOf(segmentInfo.zIndex) === -1) {
      zIndexes.push(segmentInfo.zIndex)
    }
  }

  // Render objects at each z-index level
  for (const zIndex of zIndexes) {
    let currentOffsetLeft = offsetLeft

    for (const segment of street.segments) {
      const segmentInfo = getSegmentInfo(segment.type)

      if (segmentInfo.zIndex === zIndex) {
        const variantInfo = getSegmentVariantInfo(
          segment.type,
          segment.variantString
        )
        const dimensions = getVariantInfoDimensions(variantInfo, segment.width)
        const randSeed = segment.id

        drawSegmentContents(
          ctx,
          segment.type,
          segment.variantString,
          segment.width,
          currentOffsetLeft + dimensions.left * TILE_SIZE * multiplier,
          groundLevel,
          segment.elevation,
          randSeed,
          multiplier,
          dpi
        )
      }

      currentOffsetLeft += segment.width * TILE_SIZE * multiplier
    }
  }
}

/**
 * Draws the segment names background.
 *
 *  * Draws segments.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @param {Number} multiplier - scale factor of image
 * @param {Number} groundLevel - vertical height of ground
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawSegmentNamesBackground (
  ctx,
  width,
  height,
  dpi,
  multiplier,
  groundLevel
) {
  ctx.fillStyle = BOTTOM_BACKGROUND
  ctx.fillRect(
    0,
    (groundLevel + GROUND_BASELINE_HEIGHT * multiplier) * dpi,
    width * dpi,
    (height - groundLevel - GROUND_BASELINE_HEIGHT * multiplier) * dpi
  )
}

/**
 * Draws segment names and widths.
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} dpi - pixel density of canvas
 * @param {Number} multiplier - scale factor of image
 * @param {Number} groundLevel - vertical height of ground
 * @param {Number} offsetLeft - left position to start from
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawSegmentNamesAndWidths (
  ctx,
  street,
  dpi,
  multiplier,
  groundLevel,
  offsetLeft
) {
  ctx.save()

  ctx.strokeStyle = 'black'
  ctx.lineWidth = 0.25 * dpi
  ctx.font = `normal ${SEGMENT_NAME_FONT_WEIGHT} ${
    SEGMENT_NAME_FONT_SIZE * dpi
  }px ${SEGMENT_NAME_FONT},sans-serif`
  ctx.fillStyle = 'black'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'top'

  for (const i in street.segments) {
    const segment = street.segments[i]
    const availableWidth = segment.width * TILE_SIZE * multiplier

    let left = offsetLeft

    if (i === 0) {
      left--
    }

    // Left line
    drawLine(
      ctx,
      left,
      groundLevel + GROUND_BASELINE_HEIGHT * multiplier,
      left,
      groundLevel + 125 * multiplier,
      dpi
    )

    const x = (offsetLeft + availableWidth / 2) * dpi

    // Width label
    let text = prettifyWidth(segment.width, street.units)
    let textWidth = ctx.measureText(text).width / dpi

    while (
      textWidth > availableWidth - 10 * multiplier &&
      text.indexOf(' ') !== -1
    ) {
      text = text.substr(0, text.lastIndexOf(' '))
      textWidth = ctx.measureText(text).width / dpi
    }

    ctx.fillText(text, x, (groundLevel + 60 * multiplier) * dpi)

    // Segment name label
    const name =
      segment.label || getLocaleSegmentName(segment.type, segment.variantString)
    const nameWidth = ctx.measureText(name).width / dpi

    if (nameWidth <= availableWidth - 10 * multiplier) {
      ctx.fillText(name, x, (groundLevel + 83 * multiplier) * dpi)
    }

    offsetLeft += availableWidth
  }

  // Final right-hand side line
  const left = offsetLeft + 1
  drawLine(
    ctx,
    left,
    groundLevel + GROUND_BASELINE_HEIGHT * multiplier,
    left,
    groundLevel + 125 * multiplier,
    dpi
  )

  ctx.restore()
}

/**
 * Turns drawn objects on canvas into a single-colour silhouette
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Number} width - width of area to draw
 * @param {Number} height - height of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawSilhouette (ctx, width, height, dpi) {
  ctx.globalCompositeOperation = 'source-atop'
  ctx.fillStyle = SILHOUETTE_FILL_COLOUR
  ctx.fillRect(0, 0, width * dpi, height * dpi)
}

/**
 * Draws street nameplate
 *
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on
 * @param {Object} street - street data
 * @param {Number} width - width of area to draw
 * @param {Number} dpi - pixel density of canvas
 * @modifies {CanvasRenderingContext2D} ctx
 */
function drawStreetNameplate (ctx, street, width, dpi) {
  let text = street.name || formatMessage('street.default-name', 'Unnamed St')

  ctx.textAlign = 'center'
  ctx.textBaseline = 'center'
  ctx.font = `normal ${STREET_NAME_FONT_WEIGHT} ${
    STREET_NAME_FONT_SIZE * dpi
  }px ${STREET_NAME_FONT},sans-serif`

  // Handles long names
  let measurement = ctx.measureText(text)

  let needToBeElided = false
  while (measurement.width > (width - 200) * dpi) {
    text = text.substr(0, text.length - 1)
    measurement = ctx.measureText(text)
    needToBeElided = true
  }
  if (needToBeElided) {
    text += '…'
  }

  // Street nameplate
  ctx.fillStyle = 'white'
  const x1 = (width * dpi) / 2 - (measurement.width / 2 + 45 * dpi)
  const x2 = (width * dpi) / 2 + (measurement.width / 2 + 45 * dpi)
  const y1 = (75 - 60) * dpi
  const y2 = (75 + 60) * dpi
  ctx.fillRect(x1, y1, x2 - x1, y2 - y1)

  // Street nameplate border
  ctx.strokeStyle = 'black'
  ctx.lineWidth = 5 * dpi
  ctx.strokeRect(
    x1 + 5 * dpi * 2,
    y1 + 5 * dpi * 2,
    x2 - x1 - 5 * dpi * 4,
    y2 - y1 - 5 * dpi * 4
  )

  const x = (width * dpi) / 2

  const baselineCorrection = 27
  const y = (75 + baselineCorrection) * dpi

  ctx.strokeStyle = 'transparent'
  ctx.fillStyle = 'black'
  ctx.fillText(text, x, y)
}

/**
 * Draws a "made with Streetmix" watermark on the lower right of the image.
 *
 * @todo Make it work with rtl
 * @param {CanvasRenderingContext2D} ctx - the canvas context to draw on.
 * @param {Number} dpi - pixel density of canvas
 * @param {Boolean} invert - if `true`, render light text for dark background
 * @modifies {CanvasRenderingContext2D}
 */
function drawWatermark (ctx, dpi, invert = false) {
  const text = formatMessage(
    'export.watermark',
    'Made with {streetmixWordmark}',
    {
      // Replace the {placeholder} with itself. Later, this is used to
      // render the logo image in place of the text.
      streetmixWordmark: '{streetmixWordmark}'
    }
  )
  const wordmarkImage = invert
    ? images.get('/images/wordmark_white.svg')
    : images.get('/images/wordmark_black.svg')

  // Separate string so that we can render a wordmark with an image
  const strings = text.replace(/{/g, '||{').replace(/}/g, '}||').split('||')

  // Set text render options
  ctx.textAlign = 'right'
  ctx.textBaseline = 'alphabetic'
  ctx.font = `normal ${WATERMARK_FONT_WEIGHT} ${
    WATERMARK_FONT_SIZE * dpi
  }px ${WATERMARK_FONT},sans-serif`
  ctx.fillStyle = invert ? WATERMARK_LIGHT_COLOR : WATERMARK_DARK_COLOR

  // Set starting X/Y positions so that watermark is aligned right and bottom of image
  const startRightX = ctx.canvas.width - WATERMARK_RIGHT_MARGIN * dpi
  const startBottomY = ctx.canvas.height - WATERMARK_BOTTOM_MARGIN * dpi

  // Set wordmark width and height based on image scale (dpi)
  const logoWidth = wordmarkImage.width * dpi
  const logoHeight = wordmarkImage.height * dpi

  // Keep track of where we are on the X-position.
  let currentRightX = startRightX

  // Render each part of the string.
  for (let i = strings.length - 1; i >= 0; i--) {
    const string = strings[i]

    // If we see the wordmark placeholder, render the image.
    if (string === '{streetmixWordmark}') {
      const margin = WORDMARK_MARGIN * dpi
      const logoLeftX = currentRightX - logoWidth - margin
      const logoTopY = startBottomY - logoHeight + dpi // Additional adjustment for visual alignment

      ctx.drawImage(
        wordmarkImage.img,
        logoLeftX,
        logoTopY,
        logoWidth,
        logoHeight
      )

      // Update X position.
      currentRightX = logoLeftX - margin
    } else {
      ctx.fillText(string, currentRightX, startBottomY)

      // Update X position.
      currentRightX = currentRightX - ctx.measureText(string).width
    }
  }
}

/**
 * Draws street image to 2D canvas element.
 *
 * @param {CanvasRenderingContext2D} ctx - 2D canvas context to draw on
 * @param {Object} street - Street data
 * @param {Object} params
 * @param {Number} params.width - Width of resulting image
 * @param {Number} params.height - Height of resulting image
 * @param {Number} params.dpi - Pixel density of canvas
 * @param {Number} params.multiplier - Scale factor of image
 * @param {Boolean} params.silhouette - If `true`, image is a silhouette
 * @param {Boolean} params.bottomAligned - TODO: Document this
 * @param {Boolean} params.transparentSky - If `true`, sky is transparent
 * @param {Boolean} params.segmentNamesAndWidths - If `true`, include segment names and widths
 * @param {Boolean} params.streetName - If `true`, include street nameplate
 * @param {Boolean} params.watermark - If `true`, include Streetmix watermark
 * @modifies {CanvasRenderingContext2D} ctx
 */
export function drawStreetThumbnail (
  ctx,
  street,
  {
    width,
    height,
    dpi,
    multiplier,
    silhouette,
    bottomAligned,
    transparentSky,
    segmentNamesAndWidths,
    streetName,
    watermark = true
  }
) {
  // Calculations

  // Determine how wide the street is
  let occupiedWidth = 0
  for (const segment of street.segments) {
    occupiedWidth += segment.width
  }

  let offsetTop
  if (bottomAligned) {
    offsetTop = height - 180 * multiplier
  } else {
    offsetTop = (height + 5 * TILE_SIZE * multiplier) / 2
  }
  if (segmentNamesAndWidths) {
    offsetTop -= SAVE_AS_IMAGE_NAMES_WIDTHS_PADDING * multiplier
  }

  const offsetLeft = (width - occupiedWidth * TILE_SIZE * multiplier) / 2
  const buildingOffsetLeft = (width - street.width * TILE_SIZE * multiplier) / 2

  const groundLevel = offsetTop + 135 * multiplier
  const horizonLine = groundLevel + 20 * multiplier

  // Sky
  if (!transparentSky) {
    drawSky(
      ctx,
      street,
      width,
      height,
      dpi,
      multiplier,
      horizonLine,
      groundLevel
    )
  }

  // Ground
  drawGround(ctx, street, width, dpi, multiplier, horizonLine, groundLevel)

  // Buildings
  drawBuildings(
    ctx,
    street,
    width,
    dpi,
    multiplier,
    groundLevel,
    buildingOffsetLeft
  )

  // Segments
  drawSegments(ctx, street, dpi, multiplier, groundLevel, offsetLeft)

  // Segment names background
  if (segmentNamesAndWidths || silhouette) {
    drawSegmentNamesBackground(ctx, width, height, dpi, multiplier, groundLevel)
  }

  // Segment names
  if (segmentNamesAndWidths) {
    drawSegmentNamesAndWidths(
      ctx,
      street,
      dpi,
      multiplier,
      groundLevel,
      offsetLeft
    )
  }

  // Silhouette
  if (silhouette) {
    drawSilhouette(ctx, width, height, dpi)
  }

  // Street nameplate
  if (streetName) {
    drawStreetNameplate(ctx, street, width, dpi)
  }

  // Watermark
  if (watermark) {
    drawWatermark(ctx, dpi, !segmentNamesAndWidths)
  }
}