Feedback

Please leave feedback and comments. I am always interested to hear how people get on using these LScripts!

Wednesday, 18 December 2013

LScript - Motion_Squash


LScript (Layout) to squash objects based on interaction with ground plane. I use it for quick ball animations to squash as they bounce.

Compatible with Newtek LightWave 9.6 and above.

// LScript Item Animation - www.StephenCulley.co.uk
//
// web   address: http://www.stephenculley.co.uk
// email address: email@stephenculley.co.uk

/* 
    LScript Item Animation - Squash

    Motion_Squash.ls

*/

@version 2.2
@warnings
@script motion
@name *Squash

    // Title
    sTitle = "*Squash";

    // Version
    sVersion = "v1.0";

    // Item
    GroundItem = nil; // Item
    GroundItemName = "nil"; // Item

    // Variable
    bWorldCoordinates = false;
    nRadius = 1.0;
    nStretchX = 0.33;
    nStretchZ = 0.33;

create
{
    // Description
    setdesc(sTitle);

    // Info
    info("*Squash - Apply a scaling effect based on Y axis.");
}

destroy
{
}

process: ma, frame, time
{     
    // Variables
    if(bWorldCoordinates) 
      {
      vPosition = ma.get(WPOSITION,time);
      vRotation = ma.get(ROTATION,time);
      vScale = ma.get(SCALING,time);
      vGroundItemPosition = <0,0,0>;
      vGroundItemRotation = <0,0,0>;
      vGroundItemScale = <0,0,0>;

      if(GroundItemName != "nil") {GroundItem = Mesh(GroundItemName); GroundItemName = "nil";}
      if(GroundItem)
        { 
        vGroundItemPosition = GroundItem.getWorldPosition(time);
        vGroundItemRotation = GroundItem.getRotation(time);
        vGroundItemScale = GroundItem.getScaling(time);
        }
      else
        {
        Item = ma.objID;
        ItemParent = Item.parent;
        if(ItemParent)
          {
          vGroundItemPosition = ItemParent.getWorldPosition(time);
          vGroundItemRotation = ItemParent.getRotation(time);
          vGroundItemScale = ItemParent.getScaling(time);
          }
        }

      }
    else
      {
      vPosition = ma.get(POSITION,time);
      vRotation = ma.get(ROTATION,time);
      vScale = ma.get(SCALING,time);
      vGroundItemPosition = <0,0,0>;
      vGroundItemRotation = <0,0,0>;
      vGroundItemScale = <0,0,0>;

      if(GroundItemName != "nil") {GroundItem = Mesh(GroundItemName); GroundItemName = "nil";}
      if(GroundItem)
        { 
        vGroundItemPosition = GroundItem.getPosition(time);
        vGroundItemRotation = GroundItem.getRotation(time);
        vGroundItemScale = GroundItem.getScaling(time);
        }
      else
        {
        Item = ma.objID;
        ItemParent = Item.parent;
        if(ItemParent)
          {
          vGroundItemPosition = ItemParent.getPosition(time);
          vGroundItemRotation = ItemParent.getRotation(time);
          vGroundItemScale = ItemParent.getScaling(time);
          }
        }

      }
        
  nDistance =  abs(vGroundItemPosition.y - vPosition.y);
  if(nDistance < nRadius)
    {
    // Squash
    nScale = (1 / nRadius) * nDistance;
    vScale.y := nScale;
    // Stretch
    vScale.x = 1 + ((1 - nScale) * nStretchX);
    vScale.z = 1 + ((1 - nScale) * nStretchZ);
    }

// ma

  ma.set(SCALING,vScale);

}

load: what,io
{
    if(what == SCENEMODE)   // processing an ASCII scene file
    {
        sHeader = io.read().asStr();

        if(sHeader == sTitle + " " + sVersion)
            {
            GroundItemName = io.read().asStr();
            bWorldCoordinates = io.read().asInt();
            nRadius = io.read().asNum();
            nStretchX = io.read().asNum();
            nStretchZ = io.read().asNum();
            }
    }
}

save: what,io
{
    if(what == SCENEMODE)
    {
        // Header
        io.writeln(sTitle + " " + sVersion);

        if(GroundItem != nil)
            {
            io.writeln(string(GroundItem.name));
            }
        else
            {
            io.writeln("nil");
            }    

        io.writeln(bWorldCoordinates);
        io.writeln(nRadius);
        io.writeln(nStretchX);
        io.writeln(nStretchZ);
    }
}

options
{
    if(reqisopen())
        {
        reqend();
        return;
        }
 
    reqbegin(sTitle + " " + sVersion);

    ctrl_c0 = ctlcheckbox("World Coordinates",bWorldCoordinates);
    ctrl_c1 = ctlmeshitems("Ground Item",GroundItem);
    ctrl_c2 = ctldistance("Radius",nRadius);
    ctrl_c3 = ctlpercent("Stretch X",nStretchX);
    ctrl_c4 = ctlpercent("Stretch Z",nStretchZ);

    // Developer
    ctlsep();
    ctrl_dev0 = ctltext("","developer: Stephen Culley","http://www.stephenculley.co.uk");

    // Refresh
    ctlrefresh(ctrl_c0,"refresh_c0"); // World Coordinates
    ctlrefresh(ctrl_c1,"refresh_c1"); // Ground Item
    ctlrefresh(ctrl_c2,"refresh_c2"); // Radius
    ctlrefresh(ctrl_c3,"refresh_c3"); // Stretch X
    ctlrefresh(ctrl_c4,"refresh_c4"); // Stretch Z

    reqopen();
}

refresh_c0:value // World Coordinates
{
    bWorldCoordinates = value;
}

refresh_c1:value // Ground Item
{
    GroundItem = value;
}

refresh_c2:value // Radius
{
    nRadius = value;
}

refresh_c3:value // Stretch X
{
    nStretchX = value;
}

refresh_c4:value // Stretch Z
{
    nStretchZ = value;
}
All scripts available at my Google Drive at
https://drive.google.com/open?id=1cR_q2GVUAJHumic1-A3eXV16acQnVTWs

LScript - Image_Stars


LScript (Layout) generate stars for space background image maps.

Compatible with Newtek LightWave 9.6 and above.

// LScript Image Filter - www.StephenCulley.co.uk
//
// web   address: http://www.stephenculley.co.uk
// email address: email@stephenculley.co.uk

/*  
    LScript Image Filter - Stars
    Image_Stars.ls

*/

@version 2.5
@warnings
@script image
@name *Stars

    // Title
    sTitle = "*Stars";

    // Version
    sVersion = "v1.0";

    // Variable
    Global_nOpacity = 1.0;

    Stars_vColor = <255,255,255>;
    Stars_iAmount = 200; 
    Stars_nOpacity = 0.2;
    Stars_nMinSize = 1.0;
    Stars_nMaxSize = 5.0;

create
{
    setdesc(sTitle);
}

process: ifo
{

    if(runningUnder() != SCREAMERNET) moninit(Stars_iAmount);

    vColor = Stars_vColor * (1/255);
    for(a = 1;a <= Stars_iAmount;++a)
      {
      v = ;
      circleenhancedIFO(ifo,
                        v, // Vector
                        max(Stars_nMinSize,randu() * Stars_nMaxSize), // Size
                        0.0,// fade
                        vColor.x,vColor.y,vColor.z, // Color
                        Stars_nOpacity * Global_nOpacity); // Opacity    

      if(runningUnder() != SCREAMERNET) if(monstep()) return;

      }
}

// IFO

circleenhancedIFO: ifo,v,s,f,r,g,b,o // vector,size,fade,red,green,blue,opacity
{
    // Size <= 1
    if(s <= 1)
      {
      o *= s;
      if (v.x >= 1 && v.x <= ifo.width && v.y >= 1 && v.y <= ifo.height)
        { 
        if(o >= 1.0)
          {
          ifo.red[v.x,v.y] = r;
          ifo.green[v.x,v.y] = g;
          ifo.blue[v.x,v.y] = b;
          }
        else if(o > 0.0)
          {
          ifo.red[v.x,v.y] = (ifo.red[v.x,v.y] * (1-o)) + (r * o);
          ifo.green[v.x,v.y] = (ifo.green[v.x,v.y] * (1-o)) + (g * o);
          ifo.blue[v.x,v.y] = (ifo.blue[v.x,v.y] * (1-o)) + (b * o);
          }
        }
      return; 
      }
    // Half Size
    s *= 0.5; 
    // Grid
    x0 = floor(v.x - s - 1);
    y0 = floor(v.y - s - 1);
    x1 = floor(v.x + s + 1);
    y1 = floor(v.y + s + 1);
    // Fade
    _f = s * f;
    // Draw
    for(y = y0;y <= y1;++y)
      {
      for(x = x0;x <= x1;++x)
        {
        // Wrap
        x2 = x;
        if(x2 < 1){x2 = ifo.width - abs(mod(x2,ifo.width));}
        if(x2 > ifo.width){x2 = mod(x2,ifo.width) + 1;}
        y2 = y;
        if(y2 < 1){y2 = ifo.height - abs(mod(y2,ifo.height));}
        if(y2 > ifo.height){y2 = mod(y2,ifo.height) + 1;}
        // Opacity          
        _o = 0.0;
        d = distance2D(v,);
        f_01 = maprange01(_f,s,d);     
        if(f_01 <= 0.0){_o = o;} // Inside
        else if(f_01 <= 1.0)
          {
          _o = o * (1 - f_01);
   }      
        if(_o >= 1.0)
          {
          ifo.red[x2,y2] = r;
          ifo.green[x2,y2] = g;
          ifo.blue[x2,y2] = b;
          }
        else if (_o > 0.0)
          {
          ifo.red[x2,y2] = (ifo.red[x2,y2] * (1-_o)) + (r * _o);
          ifo.green[x2,y2] = (ifo.green[x2,y2] * (1-_o)) + (g * _o);
          ifo.blue[x2,y2] = (ifo.blue[x2,y2] * (1-_o)) + (b * _o);
          }
        }
      }
}

// MAP RANGE

maprange01: n1,n2,i
{    
    if(n2-n1 == 0.0){return(0.0);}
  else
    {return((1/(n2-n1)) * (i-n1));}
}

// VECTOR 2D

clip2D: v,x0,y0,x1,y1 // b
{
    if((v.x >= x0) && (v.x <= x1) && (v.y >= y0) && (v.y <= y1)){return(false);}
  else
    return(true);
}

distance2D: v1, v2 // n
{
    // Vector
    return(sqrt(((v2.x - v1.x) * (v2.x - v1.x)) +
                ((v2.y - v1.y) * (v2.y - v1.y))));
}

linedistance2D: v1,v2,v3 // n
{
    // Vector v1 from line v2 to v3
    nSqrLineMagnitude = sqrlinemagnitude2D(v2,v3);
    if(nSqrLineMagnitude < 0.00000000000001)
      {
      return(-1.0);
      }
    u = ((v1.x - v2.x) * (v3.x - v2.x) + (v1.y - v2.y) * (v3.y - v2.y)) / nSqrLineMagnitude;
    if(u < 0.0000001 or u > 1)
      {
      x = distance2D(v1,v2);
      y = distance2D(v1,v3);
      return(min(x,y));
      }
    else
      {
      x = v2.x + u * (v3.x - v2.x);
      y = v2.y + u * (v3.y - v2.y);
      return(distance2D(v1,));
      }
}

sqrlinemagnitude2D: v1,v2 // n
{
    return((v2.x - v1.x) * (v2.x - v1.x) + (v2.y - v1.y) * (v2.y - v1.y));
}

//

load: what,io
{
    if(what == SCENEMODE)
    {
        if(io.read().asStr() == sTitle + " " + sVersion)
            {
            // Global
            Global_nOpacity = io.read().asNum();

            // Stars
            Stars_vColor = io.read().asVec();
            Stars_iAmount = io.read().asInt();
            Stars_nOpacity = io.read().asNum();
            Stars_nMinSize = io.read().asNum();
            Stars_nMaxSize = io.read().asNum();
            }
    }
}

save: what,io
{
    if(what == SCENEMODE)
    {
        // Header
        io.writeln(sTitle + " " + sVersion);

        // Global
        io.writeln(Global_nOpacity);

        // Stars
        io.writeln(Stars_vColor);
        io.writeln(Stars_iAmount); 
        io.writeln(Stars_nOpacity);
        io.writeln(Stars_nMinSize);
        io.writeln(Stars_nMaxSize);
    }
}

options
{
    reqbegin(sTitle + " " + sVersion);

    // Control

    // Global
    ctrl_g0 = ctlnumber("Global Opacity",Global_nOpacity);

    ctlsep();
    ctrl_stars0 = ctlcolor("Color",Stars_vColor);
    ctrl_stars1 = ctlinteger("Amount",Stars_iAmount);
    ctrl_stars2 = ctlnumber("Opacity",Stars_nOpacity);
    ctrl_stars3 = ctlnumber("Min Size",Stars_nMinSize);
    ctrl_stars4 = ctlnumber("Max Size",Stars_nMaxSize);

    // Developer
    ctlsep();
    ctrl_dev0 = ctltext("","developer: Stephen Culley","http://www.stephenculley.co.uk");

    return if !reqpost();

    Global_nOpacity = getvalue(ctrl_g0);

    Stars_vColor = getvalue(ctrl_stars0);
    Stars_iAmount = getvalue(ctrl_stars1);
    Stars_nOpacity = getvalue(ctrl_stars2); 
    Stars_nMinSize = getvalue(ctrl_stars3); 
    Stars_nMaxSize = getvalue(ctrl_stars4); 

    reqend();
}


All scripts available at my Google Drive at
https://drive.google.com/open?id=1cR_q2GVUAJHumic1-A3eXV16acQnVTWs

LScript - Motion_TimeReference


LScript (Layout) to quickly add motion to a channel based on time.

Compatible with Newtek LightWave 9.6 and above.

// LScript Item Animation - www.StephenCulley.co.uk
//
// web   address: http://www.stephenculley.co.uk
// email address: email@stephenculley.co.uk

/* 
    LScript Item Animation - Time Reference

    Motion_TimeReference.ls

*/

@version 2.2
@warnings
@script motion
@name *Time Reference

    // Title
    sTitle = "*Time Reference";

    // Version
    sVersion = "v1.0";

    // Variable
    aChannel = @"X","Y","Z","H","P","B","SX","SY","SZ"@; 
    iChannel = 1;
    nAmount = 1.0;

create
{
  _setdesc(); // setdesc();
}

destroy
{
}

_setdesc
{
  if(iChannel >= 1 && iChannel <= 3){setdesc(sTitle + " - Channel " + aChannel[iChannel] + " @ " + nAmount + "m/s");}
  if(iChannel >= 4 && iChannel <= 6){setdesc(sTitle + " - Channel " + aChannel[iChannel] + " @ " + nAmount + " degrees");}
  if(iChannel >= 7 && iChannel <= 9){setdesc(sTitle + " - Channel " + aChannel[iChannel] + " @ " + nAmount + "m/s");}
}

process: ma, frame, time
{   
 if(nAmount <> 0.0)
   {
    if(iChannel == 1) // X
      {
       vPosition = ma.get(POSITION,time) + ;
       // ma
       ma.set(POSITION,vPosition);
      }
    if(iChannel == 2) // Y
      {
       vPosition = ma.get(POSITION,time) + <0.0,nAmount * time,0.0>;
       // ma
       ma.set(POSITION,vPosition);
      }
    if(iChannel == 3) // Z
      {
       vPosition = ma.get(POSITION,time) + <0.0,0.0,nAmount * time>;
       // ma
       ma.set(POSITION,vPosition);
      }
    if(iChannel == 4) // H
      {
       vRotation = ma.get(ROTATION,time) + ;
       // ma
       ma.set(ROTATION,vRotation);
      }
    if(iChannel == 5) // P
      {
       vRotation = ma.get(ROTATION,time) + <0.0,nAmount * time,0.0>;
       // ma
       ma.set(ROTATION,vRotation);
      }
    if(iChannel == 6) // B
      {
       vRotation = ma.get(ROTATION,time) + <0.0,0.0,nAmount * time>;
       // ma
       ma.set(ROTATION,vRotation);
      }
    if(iChannel == 7) // SX
      {
       vScaling = ma.get(SCALING,time) + ;
       // ma
       ma.set(SCALING,vScaling);
      }
    if(iChannel == 8) // SY
      {
       vScaling = ma.get(SCALING,time) + <0.0,nAmount * time,0.0>;
       // ma
       ma.set(SCALING,vScaling);
      }
    if(iChannel == 9) // SZ
      {
       vScaling = ma.get(SCALING,time) + <0.0,0.0,nAmount * time>;
       // ma
       ma.set(SCALING,vScaling);
      }

    }
}

load: what,io
{
    if(what == SCENEMODE)   // processing an ASCII scene file
    {
        iChannel = io.read().asInt();
        nAmount = io.read().asNum();
        _setdesc(); // setdesc();
    }
}

save: what,io
{
    if(what == SCENEMODE)
    {
        io.writeln(iChannel);
        io.writeln(nAmount);
    }
}

options
{
    reqbegin(sTitle + " " + sVersion);

    ctrl_c0 = ctlchoice("Channel",iChannel,@"X","Y","Z","H","P","B","SX","SY","SZ"@);
    ctrl_c1 = ctlnumber("Amount",nAmount);

    // Developer
    ctlsep();
    ctrl_dev0 = ctltext("","developer: Stephen Culley","http://www.stephenculley.co.uk");

    return if !reqpost();

    iChannel = getvalue(ctrl_c0);
    nAmount = getvalue(ctrl_c1);
    _setdesc(); // setdesc();

    reqend();
}
All scripts available at my Google Drive at
https://drive.google.com/open?id=1cR_q2GVUAJHumic1-A3eXV16acQnVTWs

Saturday, 14 September 2013

LScript - Modeler_PointNormalToSkelegon


LScript (Modeler) to create skelegons based on the normals at point locations.

Compatible with Newtek LightWave 9.6 and above.

// LScript Modeler - www.StephenCulley.co.uk
//
// web   address: http://www.stephenculley.co.uk
// email address: email@stephenculley.co.uk

/*  
    LScript Modeler - Point Normal To Skelegon

    Modeler_PointNormalToSkelegon.ls

*/

@version 2.2
@warnings
@script modeler
@name *Point Normal To Skelegon

    // Title
    sTitle = "*Point Normal To Skelegon";

    // Version
    sVersion = "v1.0";

    // Variable
    aSelectedPoints = nil;

    ctrl_c0;

main
{

    // Store
    nSize = recall("nSize",0.1); 

    reqbegin(sTitle + " " + sVersion);

    // Reset
    ctrl_res0 = ctlbutton("Reset",50,"button_reset"); // Button Reset
    ctlsep();

    // Control
    ctrl_c0 = ctldistance("Size",nSize);

    // Developer
    ctlsep();
    ctrl_dev0 = ctltext("","developer: Stephen Culley","http://www.stephenculley.co.uk");

    return if !reqpost();

    nSize = getvalue(ctrl_c0);

    // Store
    store("nSize",nSize);

    // Selection - Point (DIRECT)
    selmode(DIRECT);
    iPointCount = pointcount();
    if(iPointCount < 1) error("None or not enough points selected.");

    editbegin();

        aSelectedPoints = points; // Store points for later use

        foreach(iSelectedPoint,aSelectedPoints)
          {
          aPolygons = nil;
          aPolygons = iSelectedPoint.polygon();
          // Error
          if(aPolygons == nil) error("Points must be connected to polygons.");
          foreach(iPolygons,aPolygons)
            {
            aPoints = nil;
            aPoints = polyinfo(iPolygons);
            // Error
            if(sizeof(aPoints) < 4) error("Polygons need a minimum of 3 points.");
          }
        }

    editend();

    undogroupbegin(); // Undo

    moninit(iPointCount,"Processing...");  // Progress Monitor

        foreach(iSelectedPoint,aSelectedPoints)
          {

    // Selection - Point (GLOBAL)
    selmode(GLOBAL);

    editbegin();

          vNorm = pointnormal(iSelectedPoint);   
          i1 = addpoint(pointinfo(iSelectedPoint)); // Returns ID
          i2 = addpoint(vNorm * nSize + pointinfo(iSelectedPoint)); // Returns ID
          aNormalPolygon[1] = i1;
          aNormalPolygon[2] = i2;
          iPolygon = addpolygon(aNormalPolygon);

    editend(); 

          // Selection - Polygon (USER)
          selmode(USER);

          selpolygon(CLEAR); 
          selpolygon(SET,POLYID,iPolygon);
          cmdseq("MakeSkelegons");
          delete(); 

          monstep(); // Progress Monitor
          }

    undogroupend(); // Undo

    info(iPointCount," skelegon(s) added.");
}

button_reset
{
    setvalue(ctrl_c0,0.1); // Size
}

pointnormal: point // v
{
    v = <0.0,0.0,0.0>;
    aPolygons = point.polygon();
    iPolygonCount = size(aPolygons);
    for(p = 1; p <= iPolygonCount; p++)
      {
      v += polynormal(aPolygons[p]);          
      }
    v /= iPolygonCount;
    return(normalize3D(v));
}

// INTERPOLATION

linear1D: n1,n2,i // i = interpolation point (0-1)
{
    return(n1 * (1 - i) + n2 * i);     
}

// VECTOR 3D

crossproduct3D: v1,v2 // v
{
    // Vector
    return();
}

distance3D: v1, v2 // n
{
    // Vector
    return(sqrt(((v2.x - v1.x) * (v2.x - v1.x)) +
                ((v2.y - v1.y) * (v2.y - v1.y)) + 
                ((v2.z - v1.z) * (v2.z - v1.z))));
}

magnitude3D: v // n
{
    // Vector
    return(sqrt((v.x * v.x) + (v.y * v.y) + (v.z * v.z)));
}

normalize3D: v // v
{
    // Vector normalize to 0 - 1
    nMagnitude = magnitude3D(v);
    if(nMagnitude <> 0) nMagnitude = 1 / nMagnitude;
    return(v * nMagnitude);
}


All scripts available at my Google Drive at
https://drive.google.com/open?id=1cR_q2GVUAJHumic1-A3eXV16acQnVTWs

Wednesday, 31 July 2013

LScript - Displace_Expand


LScript (Layout) to animate models expanding in to existence. It is best used upon non subdivided and unwelded polygons.

Compatible with Newtek LightWave 9.6 and above.

// LScript Displacement Map - www.StephenCulley.co.uk
//
// web   address: http://www.stephenculley.co.uk
// email address: email@stephenculley.co.uk

/*  
    LScript Displacement Map - Expand 

    Displace_Expand.ls

*/

@version 2.2
@warnings
@script displace
@name *Expand

    // Title
    sTitle = "*Expand";

    // Version
    sVersion = "v1.0";

    // Item
    Item;

    // Variable
    iCount = 0;
    iPoint = 0;
    iPointCount = 0;
    vPosition = <0.0,0.0,0.0>;

    // Requester
    bRequesterUpdate = false; // Update
    iRequesterFrame = -1; // Frame

    // Envelope
    envAmount; // Amount

    // Control
    ctrl_c0;

create: obj
{
    setdesc(sTitle);

    // Item
    Item = obj;

    // Envelope
    envAmount = Envelope("Amount (" + sTitle + ")",CHAN_NUMBER,sTitle); // Amount
    envAmount.persist(false);
    envAmount.createKey(0,0.0);

    // Info
    info("*Expand - Use with unwelded polygons without subdivions.");
}

destroy
{
}

newtime: id, frame, time
{

    if(reqisopen() && iRequesterFrame != frame)
        {
        bRequesterUpdate = true; // Update       

        setvalue(ctrl_c0,envAmount.value(Scene().currenttime)); // Amount

        iRequesterFrame = frame; // Frame
        bRequesterUpdate = false; // Update
        }

    // Subpatch Level
    SubPatchLevel(0,0);

    // Variable
    nAmount = clip(0.0,1.0,envAmount.value(time));
    iPointCount = Item.pointCount();
    nPoint = nAmount * iPointCount;
    nFrac = frac(nPoint);
    iPoint = nPoint - nFrac;    
    vPosition = linear1D(Item.position(Item.points[max(1,iPoint)]),Item.position(Item.points[min(iPointCount,iPoint + 1)]),nFrac);
    iCount = 0; // Count
}

flags
{
    // WORLD      LOCAL

    return(LOCAL);
}

process: da
{
    iCount++; // Count
    if(iCount <= iPoint)
      {
      }
    else
      {
      // da

      da.source[1] = vPosition.x;
      da.source[2] = vPosition.y;
      da.source[3] = vPosition.z;
      }     
}

// CLIP

clip: min,max,n
{
    if(n < min) n = min;
    if(n > max) n = max;
    return(n);
}

// INTERPOLATION

linear1D: n1,n2,i // i = interpolation point (0-1)
{
    return(n1 * (1 - i) + n2 * i);     
}

load: what,io
{
    if(what == SCENEMODE)   // processing an ASCII scene file
    {
        if(io.read().asStr() == sTitle + " " + sVersion)
            {
            // Envelope
            envAmount.load(); // Amount
            }
    }
}

save: what,io
{
    if(what == SCENEMODE)
    {    
        // Header
        io.writeln(sTitle + " " + sVersion);

        // Envelope
        envAmount.save(); // Amount
    }
}

options
{
    if(reqisopen())
        {
        reqend();
        return;
        }

    reqbegin(sTitle + " " + sVersion);

    ctrl_c0 = ctlpercent("Amount",envAmount.value(Scene().currenttime)); // Amount
    ctrl_c1e = ctlbutton("E",20,"button_c1e"); // Button

    // Developer
    ctlsep();
    ctrl_dev0 = ctltext("","developer: Stephen Culley","http://www.stephenculley.co.uk");

    // Refresh
    ctlrefresh(ctrl_c0,"refresh_c0"); // Amount

    reqopen();
}

button_c1e // Amount
{
    envAmount.edit();
}

refresh_c0:value // Amount
{
    if(bRequesterUpdate) return; // Requester Update
    setvalue(ctrl_c0,clip(0.0,1.0,value));
    iKey = envAmount.keyExists(Scene().currenttime);
    if(iKey == nil)
        {envAmount.createKey(Scene().currenttime,clip(0.0,1.0,value));}
    else
        {envAmount.setKeyValue(iKey,clip(0.0,1.0,value));}
}
All scripts available at my Google Drive at
https://drive.google.com/open?id=1cR_q2GVUAJHumic1-A3eXV16acQnVTWs