Firework Display

2005, Jan 27 Eric Amundsen - cross posting from news:comp.lang.tcl (c.l.t)

Anyone got a pure tcl firework display? I'm putting together a little program to drill my kids on their math facts (add/sub/mult/div), and I want a "reward" for getting x in a row right kind of thing. My wife is insisting on a firework display as one of the rewards. However, my life is insisting that I continue to do all those other things like work, shovel snow, blah blah blah. Consequently I don't have the time to climb the animation learning curve.

I've looked at the particles thing on the wiki (Particle System) but I don't see a "quick" way to adjust it to a firework burst and decay.

If I get a good solution I'll post it on the wiki.

Eric Amundsen
Rochester MN

RA2 Move South, Eric! This way you won't have to shovel snow and you'll have all the time in the world to study TCL animation. :-) Seriously, ULIS knows quite a lot on the subject (TCL animation; not snow removal! :-) Please post this question on his home page and I'm sure he'll be glad to help...

I hope I did not hurt your feelings by suggesting to move South since you seem to have a famous explorer of the Northern boundary in your ancestry. Any relationship with Amundsen, the Arctic explorer?

Eric Amundsen - I did solve this myself then with Firework Display. Should have updated this page. Animation was actually very easy, and I've essentially been doing it along with this type of thing - Keep a GUI alive during a long calculation.

No hurt feelings. While the lineage hasn't ever been determined with certainty, there does seem to be some affinity to cold weather in my blood (or at least my heritage) [L1 ]

-- My own reply a few days later

OK, so y'all decided to leave it as an exercise for the writer to do this one. I looked over the Particle system some more, read the helpful canvas tutorial from captaincrumb [L2 ], reviewed the canvas man page, let it all stew over the weekend, and came up with this while waiting through my kids' piano lessons yesterday.

I realize this "pure-tcl" solution uses itcl - I like itcl and consider it part of my core tcl distribution - I'm not going to argue this point.

#! /bin/env tclsh

package require Tk
package require Itcl

set ::_pi [expr {2 * acos(0)}]
set ::_2_pi [expr {$::_pi * 2}]

# this little proc nukes everything already in existance and redifines my
# classes, thus allowing me to simply resource into a console while developing

proc redef {} {
    # nuke everything
    catch {.fd stop} {}
    catch {itcl::delete class FireworkDisplay} {}
    catch {itcl::delete class Spark} {}
    catch {destroy .c} {}

    # the main animation class - keeps a list of sparks and traces to draw and
    # update

    itcl::class FireworkDisplay {
        constructor {c args} {}
        public {
            # creates new sparks and traces
            method explode {}

            # updates existing sparks and traces
            method moveSparks {}

            # start/stops the animation
            method start {} {
                set moveAfterId [after idle [itcl::code $this moveSparks]]
                set explodeAfterId [after [randInt {*}$explosionInterval] [
                    list after idle [itcl::code $this explode]]]
            method stop {} {
                after cancel $explodeAfterId
                after cancel $moveAfterId

            # various parameters to adjust the look of the display - should be
            # self explanatory

            variable lifespanRange {40 80}
            variable velocityRange {2 4}
            variable explosionInterval {500 800}
            variable numSparksRange {10 22}

        private {
            method randInt {{lower 0} {upper 1}} {return [
                expr {int(rand() * ($upper - $lower + 1) + $lower)}]}
            variable canvas {}
            variable sparks [list]
            variable explodeAfterId
            variable moveAfterId

    # could probably just be taken care of with arrays, but I like classes
    # anyway, this is a spark
    # this should probably be split into two classes, an explosion class,
    # which hold the lifespan, age and velocity,
    # and then a spark class which hold the angle of the spark
    itcl::class Spark {
        public {
            variable vel {} 
            variable sparkId {}
            variable trailId {}
            variable angle {}
            variable lifespan 30
            variable age 0


# animation update, called about 40 times per second
# (minus processing time in here and explosion creation)
itcl::body FireworkDisplay::moveSparks {} {

    # sparks might get deleted, so keep a list of sparks that don't die
    set tempSparkList [list]

    foreach spark $sparks {
        # a spark has died
        if {[$spark cget -age] > [$spark cget -lifespan]} {
            # remove it and it's trail from the canvas and destroy the object
            $canvas delete [$spark cget -sparkId]
            $canvas delete [$spark cget -trailId]
            itcl::delete object $spark
        } else {

            # spark still kicking, so compute next position
            set xDelta [expr {[$spark cget -vel] * cos([$spark cget -angle])}]
            set yDelta [expr {[$spark cget -vel] * sin([$spark cget -angle])}]
            $canvas move [$spark cget -sparkId] $xDelta $yDelta

            # and extend its trail
            foreach {x1 y1 x2 y2} [$canvas coords [$spark cget -trailId]] {}
            $canvas coords [$spark cget -trailId] $x1 $y1 [
                expr {$x2 + $xDelta}] [expr {$y2 + $yDelta}]
            $spark configure -age [expr {[$spark cget -age] + 1}]

            # and keep it alive
            lappend tempSparkList $spark
    set sparks $tempSparkList
    set moveAfterId [after 25 [list after idle [itcl::code $this moveSparks]]]

itcl::body FireworkDisplay::constructor {c args} {
    set canvas $c
    $c configure -background black

# create a new explosion
itcl::body FireworkDisplay::explode {} {
    # create is somewhere in the middle 80% of the canvas
    set centerx [randInt [expr {int([$canvas cget -width] * 0.1)}] [
        expr {int([$canvas cget -width] * 0.9)}]]
    set centery [randInt [expr {int([$canvas cget -height] * 0.1)}] [
        expr {int([$canvas cget -height] * 0.9)}]]

    # randomize the look of the sparks and trail
    set numSparks [randInt {*}$numSparksRange]
    set vel [randInt {*}$velocityRange]
    set lifespan [randInt {*}$lifespanRange]
    set sparkColor #[format %X%X [randInt 0 15] [randInt 0 15]][format %X%X [
        randInt 0 15] [
            randInt 0 15]][format %X%X [randInt 0 15] [randInt 0 15]]
    set trailColor #[format %X%X [randInt 0 15] [randInt 0 15]][
        format %X%X [randInt 0 15] [randInt 0 15]][
            format %X%X [randInt 0 15] [randInt 0 15]]

    # without this all explosions have a spark going directly to the right -
    # boring!
    set angleOffset [expr {$::_2_pi / ([randInt 1 100] * double($numSparks))}]

    # craete the sparks and trails
    for {set j 0} {$j < $numSparks} {incr j} {
        lappend sparks [Spark #auto]

        # place trail first so the spark is on top of the trail
        set trailId [$canvas create line $centerx $centery \
            $centerx $centery -fill $trailColor -width 3]
        set sparkId [$canvas create oval [expr {$centerx - 4}] [
            expr {$centery - 4}] [expr {$centerx + 4}] [
                expr {$centery + 4}] -fill $sparkColor -outline $sparkColor]
        [lindex $sparks end] configure  \
            -lifespan $lifespan     \
            -sparkId $sparkId       \
            -trailId $trailId       \
            -vel $vel       \
            -angle [expr {$j * $::_2_pi / double($numSparks) + $angleOffset}]

    set explodeAfterId [after [randInt {*}$explosionInterval] [
        list after idle [itcl::code $this explode]]]

canvas .c
foreach {w h} [wm maxsize .] {}
wm geometry . [set w]x[set h]+0+0
grid .c -column 0 -row 0 -sticky news
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
bind .c <Configure> {FireworkDisplay .fd .c}
.c configure -height $h -width $w