package displayShelf; import f3.ui.*; import f3.ui.canvas.*; import f3.ui.filter.*; import java.lang.Math; import java.lang.System; class LayoutInfo { attribute shelf: DisplayShelf; attribute scale: Number; attribute angle: Number; attribute x: Number; attribute w: Number; attribute h: Number; } trigger on LayoutInfo.w = newValue { shelf.updateLayoutLater(); } trigger on LayoutInfo.h = newValue { shelf.updateLayoutLater(); } public class DisplayShelf extends CompositeNode { public attribute popout: Number; public attribute angle: Number; public attribute selectedIndex: Integer; public attribute content: Node*; attribute kPaneOverlap: Number; attribute currentPosition: Number; attribute contentGroup: Group?; attribute layout: LayoutInfo*; attribute scrollX: Number; attribute layoutRequests: Integer; operation updateLayout(); operation updateLayoutLater(); } operation DisplayShelf.updateLayoutLater() { var req = ++layoutRequests; do later { if (req == layoutRequests) { updateLayout(); } } } attribute DisplayShelf.layout = bind foreach (i in content) LayoutInfo { shelf: this w: bind i.currentWidth h: bind i.currentHeight } ; //attribute DisplayShelf.angle = 35; //attribute DisplayShelf.popout = 0.3; attribute DisplayShelf.selectedIndex = -1; trigger on DisplayShelf.angle = value { updateLayout(); } trigger on DisplayShelf.popout = value { updateLayout(); } operation DisplayShelf.updateLayout() { var sel = Math.floor(currentPosition); var delta = currentPosition - sel; var invDelta = 1-delta; var invPopout = 1-popout; var x = 0; var p = Math.sqrt(Math.abs(angle)/90); var scaleForAngle = 1 - p; var sx = 0; var margin = 3; for (i in layout) { var delta1 = bind indexof i - currentPosition; var myAngle = -delta1*angle; i.angle = if myAngle < -angle then -angle else if myAngle > angle then angle else myAngle; var fixedScale = if indexof i == sel then 1 else invPopout; var lowerScale = invDelta * popout + invPopout; var upperScale = delta * popout + invPopout; var variableScale = if indexof i == sel then lowerScale else if indexof i == sel + 1 then upperScale else invPopout; var scaleValue = if delta == 0 then fixedScale else variableScale; i.scale = scaleValue; var w = contentGroup.content[indexof i].currentWidth; if (indexof i == sel) { // update scroll position sx = -x - delta * w + margin; } else if (indexof i == sel+1) { // leave a gap var w1 = contentGroup.content[indexof i-1].currentWidth; var unscaledWidth = content[indexof i].currentWidth; x += (unscaledWidth + unscaledWidth*scaleForAngle*invPopout)-(w1 + w); } i.x = x; x += w + margin*i.scale; } scrollX = sx; } trigger on DisplayShelf.selectedIndex[old] = value { if (old == -1) { currentPosition = value; updateLayoutLater(); return; } operation easeOutCubic(time, begin, change, duration) { var r = time / duration - 1; return (change * (r * r * r + 1) + begin); } operation easeOutQuad(t, b, c, d) { t /= d; return -c *t*(t-2) + b; } var dx = value - old; var absDx = Math.abs(dx); var duration = if absDx == 1 then 1000 else 1400; var ease = if absDx == 1 then easeOutCubic else easeOutQuad; for (i in [1..100]) (dur duration linear while selectedIndex == value) { var c = ease(i, old, dx, 100); if (c <> currentPosition) { currentPosition = c; updateLayout(); } } } operation DisplayShelf.composeNode() { updateLayoutLater(); return Group { content: Group { attribute: contentGroup content: bind foreach (l in layout) Group { var c = bind content[indexof l] var scaleValue = bind l.scale var t = TiltingPane { cursor: HAND onMousePressed: operation(e) { selectedIndex = indexof l; } angle: bind l.angle content: bind c } content: t opacity: bind scaleValue transform: bind [translate(l.x, t.currentHeight/2 - t.currentHeight/2 * scaleValue), scale(scaleValue, scaleValue)] } transform: bind translate(scrollX, 0) } }; } class Model { attribute selectedIndex: Integer; attribute imageUrls: String*; } Frame { onClose: operation() {System.exit(0);} var model = Model { var urls = ["http://a1.phobos.apple.com/r10/Music/05/7d/c3/dj.umbuvrfe.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/cb/9a/b3/mzi.krksguze.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/94/8d/83/dj.jionwnuf.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/26/43/02/dj.dgnjindw.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/69/2a/63/mzi.wpfmtfzp.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/17/e1/88/dj.gcajwhco.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/21/f6/32/dj.glzycglj.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/d1/6b/3b/mzi.pajmxsmk.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/f6/a7/b2/dj.lamcsbwx.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/84/a5/4f/dj.nqvsikaq.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/7d/c3/23/dj.elyzoipc.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/80/a5/8c/dj.oidpsvzg.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/d1/b2/cf/dj.moyzjiht.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/49/a3/59/mzi.ssjpuxwt.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/b1/4f/c8/dj.uadqyjbr.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/d4/31/df/mzi.pqzeferc.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/4b/88/a7/dj.jhotijvb.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/a8/a9/36/dj.asztraij.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/d6/6b/c4/mzi.dricykdh.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Features/d4/81/a3/dj.tpysowpf.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/4f/2c/a6/dj.cawuddxy.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/d8/9c/8a/mzi.vmajyyha.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/00/5c/31/mzi.tuyoxwib.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/da/c8/e2/mzi.sanzeosx.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/43/cc/0e/dj.zfqfgoas.200x200-75.jpg", "http://a1.phobos.apple.com/r10/Music/73/70/13/mzi.uswlslxx.200x200-75.jpg"] imageUrls: urls selectedIndex: sizeof urls/2 } title: "F3 Display Shelf" height: 400 width: 700 visible: true content: Canvas { focusable: true focusTraversalKeysEnabled: false onKeyDown: operation(e:KeyEvent) { if (e.keyStroke == LEFT:KeyStroke) { model.selectedIndex = Math.max(0, model.selectedIndex-1); } else if (e.keyStroke == RIGHT:KeyStroke) { model.selectedIndex = Math.min(sizeof model.imageUrls-1, model.selectedIndex+1); } else if (e.keyStroke == PAGE_DOWN:KeyStroke) { var i = model.selectedIndex + 5; model.selectedIndex = Math.min(i, sizeof model.imageUrls-1); } else if (e.keyStroke == PAGE_UP:KeyStroke) { var i = model.selectedIndex - 5; model.selectedIndex = Math.max(0, i); } else if (e.keyStroke == HOME:KeyStroke) { model.selectedIndex = 0; } else if (e.keyStroke == END:KeyStroke) { model.selectedIndex = sizeof model.imageUrls - 1; } } background: black content: Group { transform: translate(-300, 20) content: [Group { content: DisplayShelf { transform: translate(575, 0) angle: 40 popout: 0.3 selectedIndex: bind model.selectedIndex content: foreach (u in model.imageUrls) Clip { shape: Rect {height: 350, width: 133} var im = ImageView { antialias: false image: {url: u} } var im2 = ImageView { antialias: false selectable: false image: {url: u} } content: [im, Group { transform: bind [translate(0, 300), scale(1, -0.5)] content: im2 filter: OpacityMask { fill: LinearGradient { x1: 0, y2: 0, x2: 0, y2: 1 stops: [Stop { offset: 0 color: new Color(0, 0, 0, 0) }, Stop { offset: 0.5 color: new Color(0, 0, 0, 0.3) }] } } }] } } }, ITunesScrollBar { isSelectionRoot: true selection: bind model.selectedIndex itemCount: bind sizeof model.imageUrls cursor: DEFAULT width: 500 viewportWidth: 15 viewSize: bind sizeof model.imageUrls pageSize: 5 transform: translate(650, 275) halign: CENTER }] } } }