Pen Settings



CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource


Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource


Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.


Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.






                // REFERENCES for ZIM at
// see for an intro example
// see for video and code tutorials
// see for documentation
// see for ZIM on CodePen

// *** NOTE: since ZIM Cat (before ZIM NFT) ZIM defaults to time in seconds
// All previous versions, examples, videos, etc. have time in milliseconds
// This can be set back with TIME = "milliseconds" but we suggest you give it a try!
// There will be a warning in the conslole if your animation is not moving ;-)
// put your code here

// ~~~~~~~~~~~~~~~~ FRAME ~~~~~~~~~~ 
// will use FULL mode 
// most of the CodePen ZIM apps are in FIT mode 
// which means we do not have to worry about scaling 
// With FULL mode we will scale things at the bottom in a resize event 
// For more options, also see the Layout class which is made to scale things in FULL mode
// but this app just had a simple top and bottom bar
// We have not scaled to content... but might want to for smaller mobile 
// but the circles are not that big so it fits well on all devices

const frame = new Frame({
	assets:{font:"reuben", src:"Reuben.otf"}, 

frame.on("ready", () => {
	const stage = frame.stage;
	let stageW = frame.width;
	let stageH = frame.height;

	// ~~~~~~~~~~~~~~~~ TOP BAR ~~~~~~~~~~ 
	// here is the top bar - we will scale the backing in the resize event at the bottom 
	const topBar = new Container(stageW, 70).addTo();
	topBar.backing = new Rectangle(stageW, 70, darker).addTo(topBar).sha(null,0,5,15)
	frame.makeIcon(null,darker).scaleTo(topBar,null,80).pos(30,0,LEFT,CENTER,topBar).tap(() => {
		zgo("", "_blank")
	const updates = new Button({
	}).sca(.4).alp(.7).tap(()=>{zgo("", "_black");}); // will position in resize
	// used place then commented it out
	new Label("TIMELINE", 20, "reuben", light).loc(76, 41, topBar); //.place(); 

	// ~~~~~~~~~~~~~~~~ HAPPENINGS ~~~~~~~~~~ 
	// We call the events happenings (or circles) 
	// We are going to spread them along a ZIM Squiggle() using ZIM Beads()
	// Each bead will be made by calling the happening function 
	// Here we prepare the data for the happenings    

	// The colors will be in a series so they repeat 
	// we could use an array then use color = colors[i%colors.length] 
	// but ZIM has a series to make that easier 
	// every time we call colors() it gets the next one in the series  
	const colors = series(pink.darken(.3), green.darken(.3), orange.darken(.3));

	// We will use real date data for the different versions of ZIM
	// month is 0 indexed
	const dates = [
		new Date(2014,10,24),   // one
		new Date(2015,11,23),   // duo
		new Date(2016,3,16),    // tri
		new Date(2016,8,15),    // 4th
		new Date(2017,5,5),     // vee
		new Date(2017,8,9),     // six        
		new Date(2018,1,6),     // hep
		new Date(2018,6,28),    // oct
		new Date(2018,8,25),    // nio
		new Date(2019,1,10),    // ten        
		new Date(2020,4,1),     // cat
		new Date(2021,6,21),    // nft        
		new Date(2022,3,11)     // zim        

	// convert the dates to percents which is what the beads use
	const percents = [];

	// dates can be converted to seconds from 1970 with the JS getTime()
	const firstTime = dates[0].getTime();
	const lastTime = dates[dates.length-1].getTime();
	const range = lastTime-firstTime;
	loop(dates, date=>{
	const names = ["ONE","DUO","TRI","4TH","VEE","SIX","HEP","OCT","NIO","TEN","CAT","NFT","ZIM"]
	let count = 0;

	function happening() {
		let circle = new Circle(100,colors(),lighter,5);
		STYLE = {align:CENTER, valign:CENTER, color:lighter}
		new Label("ZIM\n"+names[count], null, "reuben").centerReg(circle).mov(0,-20);
		new Label(dates[count].toLocaleString('en-us',{month:'short', year:'numeric'}), 20).centerReg(circle).mov(0,40)
		STYLE = {}
		// will turn this on when the happening is selected
		circle.highLight = new Circle(circle.radius+20,white).addTo(circle).bot().alp(0)
		circle.cur().tap((e) => {
		return circle; // gets added as a bead

	// ~~~~~~~~~~~~~~~~ PATH AND BEADS ~~~~~~~~~~    
	// Here is the path that the Beads will follow 
	// made at
	const points = [[34.6,23.4,0,0,-12.8,18.7,12.8,-18.7],[79.9,-10.8,0,0,-24.6,-3,24.6,3],[118.4,15.6,0,0,-21.7,-1,21.7,1],[153,-9.8,0,0,-20.7,4.9,20.7,-4.9],[189.5,5.7,0,0,-17.7,0,17.7,0],[219.1,-27.6,0,0,-16.7,6.9,16.7,-6.9],[258.6,-15.9,0,0,-17.7,0,17.7,0],[289.2,-39.4,0,0,0,0,0,0]];
	const path = new Squiggle({points:points, thickness:10})
		.transformPoints("scaleX", 8) // scales points
		.transformPoints("scaleY", 6)
		.sca(3) // scales everything      

	const beads = new Beads({
		clone:false // IMPORTANT - so Beads keep the circle made in function

	// make a white line follow the path - just for the look!
	const line = path.clone().loc(path,null,beads).bot().ord(1);    
	line.thickness = 2;
	line.color = white;

	// ~~~~~~~~~~~~~~~~ SWIPER ~~~~~~~~~~     
	// normally we use drag() to drag things 
	// but here we use a couple swipers to easily move the happenings around
	const swiperX = new Swiper(stage, beads, "x");
	const swiperY = new Swiper(stage, beads, "y", null, VERTICAL);

	// ~~~~~~~~~~~~~~~~ BOTTOM BAR ~~~~~~~~~~    
	// prepare the controls for the bottom bar

	const left = new Arrow().rot(180).tap(()=>{go(LEFT);});

	const right = new Arrow().tap(()=>{go(RIGHT);});

	const indicator = new Indicator({
	}).change(() => {

	const botBar = new Tile([left,indicator,right],3,1,0,0,true)
	botBar.widthStart = botBar.width;

	// ~~~~~~~~~~~~~~~~ GO FUNCTION ~~~~~~~~~~ 
	let lastCircle; // will use to deselect last selected
	let animating = false; // do not want to interupt animation
	let waiting = null; // will remember if want to go again

	// we might be going left or right - or pass in a specific happening
	function go(dir, num) { 
		if (animating) { // do not interupt animation
			waiting = dir; // we check this once animation is done
		waiting = null;
		animating = true;   
		swiperX.enabled = false; // do not swipe while animating
		swiperY.enabled = false; 
		indicator.enabled = false;
		let location = getLocation(dir, num); // find out where to animate    
			// make for shorter time with shorter distance, etc.
			time:constrain(Math.max(Math.abs(location.x),Math.abs(location.y))/500, 0, 3),
			call:() => {
				// swipers use damping
				// set immediate or they will try and go to last remembered location
				swiperX.enabled = true;
				swiperY.enabled = true;
				indicator.enabled = true;
				testEnds(); // set the arrows if past the ends
				animating = false;
				// here is where we help the user experience
				// otherwise, it is a pain to keep waiting for the animation to finish 
				// to make the next move - this at least seems to help a little 
				// so the user can keep clicing and it flows quickly from one to the next
				if (waiting) go(waiting); 

	// ~~~~~~~~~~~~~~~~ TESTING ARROWS ~~~~~~~~~~ 
	swiperX.on("swipestop", testEnds);
	function testEnds() {
		// circles are inside beads which moves around 
		// so use localToGlobal() on the first and last centers 0,0 for a circle 
		// and compare to stageW/2 to see if we need to grey out arrows
		let point1 = beads.items[0].localToGlobal(0,0);
		let point2 = beads.items[beads.items.length-1].localToGlobal(0,0);
		left.activate(point1.x < stageW/2-5);
		right.activate(point2.x > stageW/2+5);

	// ~~~~~~~~~~~~~~~~ CALCULATING LOCATIONS ~~~~~~~~~~ 
	function getLocation(dir, num) {        
		if (num==null) {
			if (dir==null) dir = RIGHT;
			num = dir==LEFT?beads.items.length-1:0;
			// loop through the beads to find the next one to go to
			// if going right then it will be the next circle past the center 
			// if going left then it will be the next circle before the center
			let val = beads.beads.loop((bead, i)=>{
				// circles are in beads so this locates their center on the stage
				let point = bead.localToGlobal(0,0);
				// if this is the next in the direction we are going return it to val 
				// returning a value stops the loop - like break in for loop
				// if we wanted to go to the next loop (like continue) we would just return (with no value)
				if (dir==RIGHT && point.x > stageW/2+5) return i;
				else if (dir==LEFT && point.x < stageW/2-5) return i;
			}, dir==LEFT); // if direction is LEFT then loop backwards

			if (val===true) { // this is if no circle is found
				let point1 = beads.items[0].localToGlobal(0,0);
				if (point1.x > stageW/2-5) num = 0;
				let point2 = beads.items[beads.items.length-1].localToGlobal(0,0);
				if (point2.x < stageW/2+5) num = beads.items.length-1;
			} else if (val>=0) num = val;
		indicator.selectedIndex = num;
		// animate out and in a selection 
		if (lastCircle) lastCircle.highLight.animate({alpha:0},.5);
		lastCircle = beads.items[num]; // remember what is selected for next time
		// finally calculate the relative distance to the center 
		// relative distances are passed in to animate as strings so convert to strings 
		// usually animation is to an absolute postion (numbers) but here we chose relative
		const point = beads.items[num].localToGlobal(0,0);         
		return {x:String(stageW/2-point.x), y:String(stageH/2-point.y)};
	}; // make top layer
	go(null,0); // go to the first happening

	// ~~~~~~~~~~~~~~~~ RESIZE ~~~~~~~~~~ 
	frame.on("resize", () => {
		stageW = frame.width; // get the new stageW and stageH
		stageH = frame.height;
		topBar.backing.widthOnly = stageW; // widthOnly not width as width keeps aspect ratio
		// here is how we can set min and max sizes but still scale if needed
		botBar.width = constrain(stageW*.8, 200, botBar.widthStart)
		// keep the bottom bar centered on the bottom

	stage.update(); // this is needed to show any changes


}); // end of ready