Recreating the Star Wars opening crawl in AutoCAD using F# – Part 2

After introducing this series in the last post, today we're going to address the first 3 items on our TODO list:

  1. The initial blue text
  2. The theme music
  3. The star field
  4. The disappearing Star Wars logo
  5. The crawling text

The following two items are fairly significant, in their own right, so they'll each take a post of their own to complete. Oh, and I've thrown in a surprise item 6, which I'll unveil when we implement the crawling text.

Before we dive in, it's important to make some points about the code: because this is mainly just a bit of fun, this code hasn't been generalised to work with any drawing and any view, etc. I've hardcoded a lot of values that just work on a specific drawing and on my system. The timing works well for me, but may be off when working on systems with different performance profiles. Mileage may vary, as they say.

That's one reason I'm providing a drawing with the appropriate views set up. I originally planned on putting the layers in there, too, but then decided to create those at runtime (as it was simple to do so). I could have done that with the views, too, but it didn't seem worth the extra effort.

Another thing I should mention: in my first run at this I used transient graphics to display the intro and crawl text. I then decided to switch to db-resident objects, as I thought I could place them on layers that I turn off when they're no longer needed. I ended up finding that didn't work โ€“ as I have a single transaction making all the drawing modifications in the command, and couldn't find a way to have the graphics system reflect the pending database changes โ€“ so I went ahead and erased them instead. I decided to stick with db-resident rather than transient graphics, nonetheless, but using transient graphics remains a viable approach for this: the reason I'm not doing so isn't especially significant.

With that, here's a look at the code in this post running inside AutoCAD:

A few task-specific comments:

1. The initial blue text

This was straightforward to implement. I ended up creating MText with the font information embedded in the contents, rather than a separate style. This is mainly because it's the approach I used for the crawl text, later on, which uses multiple fonts. Overkill for this text, of course, but it saves me creating the style.

We're using the intro text to do a bunch of things, behind the scenes. It's essentially our splash screen for doing things like downloading the MP3 file for the theme music, etc.

2. The theme music

I came across an online version of the crawl music accessed in this great HTML implementation of the opening crawl. Seems like an ideal candidate for SWAPI integration, extending it beyond the single episode. ๐Ÿ™‚ Here's the code associated with it, if you want to take a look.

I found out that the System.Windows.Media namespace contains a MediaPlayer object that allows you to access/play an MP3 file via a URL. All that remained was to work out some of the timings related to the music โ€“ it starts at 8.5 second in, for instance โ€“ and apply these to the code. In this version of the code, the player stops either when the music finishes or when AutoCAD is closed, whichever happens first.

3. The star field

This was a fairly simple matter of generating a bunch of random numbers and using them to define stars. I didn't want to pass around large numbers of AutoCAD objects, though โ€“ whether DBPoints or even Point3ds โ€“ so I used a list of F# tuples (with x and y values, each of which holds a float between 0 and 1) that later gets transformed into DBPoints somewhere in the screen space. Although for the surprise item 6 I extended that space to be twice as high as currently needed.

Here's the F# code implementing our first pass at the EPISODE command:

module StarWars.Crawler

 

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.ApplicationServices.Core

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry

open Autodesk.AutoCAD.Runtime

open System

open System.Windows.Media

 

// The intro music MP3 file

 

let mp3 =

  "http://s.cdpn.io/1202/Star_Wars_original_opening_crawl_1977.mp3"

 

// The layers we want to create as a list of (name, (r, g, b))

 

let layers =

  [

    ("Stars", (255, 255, 255));

    ("Intro", (75, 213, 238))

  ]

 

// Create layers based on the provided names and colour values

// (only creates layers if they don't already exist... could be

// updated to make sure the layers are on/thawed and have the

// right colour values)

 

let createLayers (tr:Transaction) (db:Database) =

  let lt =

    tr.GetObject(db.LayerTableId, OpenMode.ForWrite) :?> LayerTable

  layers |>

  List.iter (fun (name, (r, g, b)) ->

    if not(lt.Has(name)) then

      let lay = new LayerTableRecord()

      lay.Color <-

        Autodesk.AutoCAD.Colors.Color.FromRgb(byte r, byte g, byte b)

      lay.Name <- name

      lt.Add(lay) |> ignor
e

      tr.AddNewlyCreatedDBObject(lay, true)

    )

 

// Get a view by name

 

let getView (tr:Transaction) (db:Database) (name:string) =

  let vt =

    tr.GetObject(db.ViewTableId, OpenMode.ForRead) :?> ViewTable

  if vt.Has(name) then

    tr.GetObject(vt.[name], OpenMode.ForRead) :?> ViewTableRecord

  else

    null

 

// Add an entity to a block and a transaction

 

let addToDatabase (tr:Transaction) (btr:BlockTableRecord) o =

  btr.AppendEntity(o) |> ignore

  tr.AddNewlyCreatedDBObject(o, true)

 

// Flush the graphics for a particular document

 

let refresh (doc:Document) =

  doc.TransactionManager.QueueForGraphicsFlush()

  doc.TransactionManager.FlushGraphics()

 

// Transform between the Display and World Coordinate Systems

 

let dcs2wcs (vtr:AbstractViewTableRecord) =

  Matrix3d.Rotation(-vtr.ViewTwist, vtr.ViewDirection, vtr.Target) *

  Matrix3d.Displacement(vtr.Target - Point3d.Origin) *

  Matrix3d.PlaneToWorld(vtr.ViewDirection)

 

// Poll until a music file has downloaded fully

// (could sleep or use a callback to avoid this being too

// CPU-intensive, but hey)

 

let rec waitForComplete (mp:MediaPlayer) =

  if mp.DownloadProgress < 1. then

    System.Windows.Forms.Application.DoEvents()

    waitForComplete mp

 

// Poll until a specified delay has elapsed since start

// (could sleep or use a callback to avoid this being too

// CPU-intensive, but hey)

 

let rec waitForElapsed (start:DateTime) delay =

  let elapsed = DateTime.Now - start

  if elapsed.Seconds < delay then

    System.Windows.Forms.Application.DoEvents()

    waitForElapsed start delay

 

// Create the intro text as an MText object relative to the view

// (has a parameter to the function doesn't execute when loaded...

// also has hardcoded values that make it view-specific)

 

let createIntro _ =

 

  let mt = new MText()

  mt.Contents <-

    "{\\fFranklin Gothic Book|b0|i0|c0|p34;" +

    "A long time ago, in a galaxy far,\\Pfar away...}"

  mt.Layer <- "Intro"

  mt.TextHeight <- 0.5

  mt.Width <- 10.

  mt.Normal <- Vector3d.ZAxis

  mt.TransformBy(Matrix3d.Displacement(new Vector3d(1., 6., 0.)))

  mt

 

// Generate a quantity of randomly located stars... a list of (x,y)

// tuples where x and y are between 0 and 1. These will later

// get transformed into the relevant space (on the screen, etc.)

 

let locateStars quantity =

 

  // Create our random number generator

 

  let ran = new System.Random()

 

  // Note: _ is only used to make sure this function gets

  // executed when it is called... if we have no argument

  // it's a value that doesn't require repeated execution

 

  let randomPoint _ =

 

    // Get random values between 0 and 1 for our x and y coordinates

 

    (ran.NextDouble(), ran.NextDouble())

 

  // Local recursive function to create n stars at random

  // locations (in the plane of the screen)

 

  let rec randomStars n =

    match n with

 
   | 0 -> []

    | _ -> (randomPoint 0.) :: randomStars (n-1)

 

  // Create the specified number of stars at random locations

 

  randomStars quantity

 

// Take locations from 0-1 in X and Y and place them

// relative to the screen

 

let putOnScreen wid hgt dcs (x, y) =

 

  // We want to populate a space that's 2 screens high (so we

  // can pan/rotate downwards at the end of the crawl), hence

  // the additional multiplier on y

 

  let pt = new Point3d(wid * (x - 0.5), hgt * ((y * -1.5) + 0.5), 0.)

  pt.TransformBy(dcs)   

 

// Commands to recreate the open crawl experience for a selected

// Star Wars episode

 

[<CommandMethod("EPISODE")>]

let episode() =

 

  // Make sure the active document is valid before continuing

 

  let doc = Application.DocumentManager.MdiActiveDocument

  if doc <> null then

 

    let db = doc.Database

    let ed = doc.Editor

 

    // Start our transaction and create the required layers

 

    use tr = doc.TransactionManager.StartTransaction()

    createLayers tr db

 

    // Get our special Initial and Crawl views

 

    let ivtr = getView tr db "Initial"

    let cvtr = getView tr db "Crawl"

 

    if ivtr = null || cvtr = null then

      ed.WriteMessage(

        "\nPlease load StarWarsCrawl.dwg before running command.")

 

    doc.TransactionManager.EnableGraphicsFlush(true)

    let btr =

      tr.GetObject(doc.Database.CurrentSpaceId, OpenMode.ForWrite)

        :?> BlockTableRecord

 

    // Set the initial view: this gives us higher quality text

 

    ed.SetCurrentView(ivtr)

 

    // First we create the intro text

 

    let intro = createIntro ()

    intro |> addToDatabase tr btr

 

    // Make sure the intro text is visible

 

    doc |> refresh

    ed.UpdateScreen()

 

    // We'll now perform a number of start-up tasks, while our

    // initial intro text is visible... we'll start vy recording

    // our start time, so we can synchronise our delay

 

    let start = DateTime.Now

 

    // Get our view's DCS matrix

 

    let dcs = dcs2wcs(cvtr)

 

    // Create a host of stars at random screen positions

 

    locateStars 1000 |>

    List.iter

      (fun xy ->

        let p = putOnScreen cvtr.Width cvtr.Height dcs xy

        let dbp = new DBPoint(p)

        dbp.Layer <- "Stars"

        dbp |> addToDatabase tr btr)

 

    // Open the intro music over the web

 

    let mp = new MediaPlayer()

    mp.Open(new Uri(mp3))

 

    // Wait for the download to compl
ete before playing it

 

    waitForComplete mp

 

    // Have a minimum delay of 5 seconds showing the intro text

 

    waitForElapsed start 5

 

    // Start the audio at 8.5 seconds in

 

    mp.Position <- new TimeSpan(0, 0, 0, 8, 500)

    mp.Play()

 

    // Switch to the crawl view: this will also change the

    // visual style from 2D Wireframe to Realistic

 

    ed.SetCurrentView(cvtr)

 

    // Remove the intro text

 

    intro.Erase()

 

    tr.Commit() // Commit the transaction

That's it for this part in the series. In the next part we'll take a look at the code to show the disappearing Star Wars logo inside AutoCAD.

Leave a Reply

Your email address will not be published. Required fields are marked *