Increase pigginess in new detail UI.

Change-Id: I4eb1acf7e6cd114edf7c5f66fcb4fb6ff8842bdb
This commit is contained in:
Aaron Boodman 2013-12-31 15:25:19 -08:00
parent e0773c28d1
commit 2d70e6794f
12 changed files with 237 additions and 104 deletions

View File

@ -345,7 +345,8 @@ func genEmbeds() error {
uiEmbeds := buildSrcPath("server/camlistored/ui")
serverEmbeds := buildSrcPath("pkg/server")
reactEmbeds := buildSrcPath("third_party/react")
for _, embeds := range []string{uiEmbeds, serverEmbeds, reactEmbeds} {
glitchEmbeds := buildSrcPath("third_party/glitch")
for _, embeds := range []string{uiEmbeds, serverEmbeds, reactEmbeds, glitchEmbeds} {
args := []string{embeds}
cmd := exec.Command(cmdName, args...)
cmd.Env = append(cleanGoEnv(),

View File

@ -40,6 +40,7 @@ import (
"camlistore.org/pkg/sorted"
uistatic "camlistore.org/server/camlistored/ui"
closurestatic "camlistore.org/server/camlistored/ui/closure"
glitchstatic "camlistore.org/third_party/glitch"
reactstatic "camlistore.org/third_party/react"
)
@ -57,6 +58,7 @@ var (
treePattern = regexp.MustCompile(`^tree/([^/]+)(/.*)?$`)
closurePattern = regexp.MustCompile(`^closure/(([^/]+)(/.*)?)$`)
reactPattern = regexp.MustCompile(`^react/(.+)$`)
glitchPattern = regexp.MustCompile(`^glitch/(.+)$`)
disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE"))
)
@ -91,8 +93,9 @@ type UIHandler struct {
uiDir string // if sourceRoot != "", this is sourceRoot+"/server/camlistored/ui"
closureHandler http.Handler
fileReactHandler http.Handler
closureHandler http.Handler
fileReactHandler http.Handler
fileGlitchHandler http.Handler
}
func init() {
@ -213,10 +216,14 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
}
if ui.sourceRoot != "" {
ui.fileReactHandler, err = ui.makeEmbeddedReactHandler(ui.sourceRoot)
ui.fileReactHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "react"), "react.js")
if err != nil {
return nil, fmt.Errorf("Could not make react handler: %s", err)
}
ui.fileGlitchHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "glitch"), "npc_piggy__x1_walk_png_1354829432.png")
if err != nil {
return nil, fmt.Errorf("Could not make glitch handler: %s", err)
}
}
rootPrefix, _, err := ld.FindHandlerByType("root")
@ -237,15 +244,6 @@ func (ui *UIHandler) makeClosureHandler(root string) (http.Handler, error) {
return makeClosureHandler(root, "ui")
}
func (ui *UIHandler) makeEmbeddedReactHandler(root string) (http.Handler, error) {
path := filepath.Join("third_party", "react")
h, err := makeFileServer(root, path, "react.js")
if h != nil {
log.Printf("Serving React from %s", path)
}
return h, err
}
// makeClosureHandler returns a handler to serve Closure files.
// root is either:
// 1) empty: use the Closure files compiled in to the binary (if
@ -277,11 +275,7 @@ func makeClosureHandler(root, handlerName string) (http.Handler, error) {
}
path := filepath.Join("third_party", "closure", "lib", "closure")
fs, err := makeFileServer(root, path, filepath.Join("goog", "base.js"))
if fs != nil {
log.Printf("%v: serving Closure from disk: %s", handlerName, filepath.Join(root, path))
}
return fs, err
return makeFileServer(root, path, filepath.Join("goog", "base.js"))
}
func makeFileServer(sourceRoot string, pathToServe string, expectedContentPath string) (http.Handler, error) {
@ -344,18 +338,10 @@ func wantsDetailPage(req *http.Request) bool {
return httputil.IsGet(req) && httputil.PathSuffix(req) == "detail.html"
}
func wantsClosure(req *http.Request) bool {
func getSuffixMatches(req *http.Request, pattern *regexp.Regexp) bool {
if httputil.IsGet(req) {
suffix := httputil.PathSuffix(req)
return closurePattern.MatchString(suffix)
}
return false
}
func wantsReact(req *http.Request) bool {
if httputil.IsGet(req) {
suffix := httputil.PathSuffix(req)
return reactPattern.MatchString(suffix)
return pattern.MatchString(suffix)
}
return false
}
@ -375,10 +361,12 @@ func (ui *UIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ui.serveThumbnail(rw, req)
case strings.HasPrefix(suffix, "tree/"):
ui.serveFileTree(rw, req)
case wantsClosure(req):
case getSuffixMatches(req, closurePattern):
ui.serveClosure(rw, req)
case wantsReact(req):
ui.serveReact(rw, req)
case getSuffixMatches(req, reactPattern):
ui.serveFromDiskOrStatic(rw, req, reactPattern, ui.fileReactHandler, reactstatic.Files)
case getSuffixMatches(req, glitchPattern):
ui.serveFromDiskOrStatic(rw, req, glitchPattern, ui.fileGlitchHandler, glitchstatic.Files)
default:
file := ""
if m := staticFilePattern.FindStringSubmatch(suffix); m != nil {
@ -570,20 +558,21 @@ func (ui *UIHandler) serveClosure(rw http.ResponseWriter, req *http.Request) {
ui.closureHandler.ServeHTTP(rw, req)
}
func (ui *UIHandler) serveReact(rw http.ResponseWriter, req *http.Request) {
// serveFromDiskOrStatic matches rx against req's path and serves the match either from disk (if non-nil) or from static (embedded in the binary).
func (ui *UIHandler) serveFromDiskOrStatic(rw http.ResponseWriter, req *http.Request, rx *regexp.Regexp, disk http.Handler, static *fileembed.Files) {
suffix := httputil.PathSuffix(req)
m := reactPattern.FindStringSubmatch(suffix)
m := rx.FindStringSubmatch(suffix)
if m == nil {
httputil.ErrorRouting(rw, req)
return
panic("Caller should verify that rx matches")
}
file := m[1]
if ui.fileReactHandler != nil {
if disk != nil {
req.URL.Path = "/" + file
ui.fileReactHandler.ServeHTTP(rw, req)
disk.ServeHTTP(rw, req)
} else {
serveStaticFile(rw, req, reactstatic.Files, file)
serveStaticFile(rw, req, static, file)
}
}
// serveDepsJS serves an auto-generated Closure deps.js file.

View File

@ -9,10 +9,17 @@ body {
overflow: hidden;
}
.detail-view-preview {
.detail-view-img {
position: absolute;
width: auto;
height: auto;
}
.detail-img-enter {
transition: opacity 200ms linear;
opacity: 0;
}
.detail-img-enter.detail-img-enter-active {
opacity: 1;
}
.detail-view-sidebar {
@ -24,6 +31,19 @@ body {
top: 0;
}
.detail-view-piggy {
position: absolute;
}
.detail-piggy-leave {
transition: opacity 200ms linear;
opacity: 1;
}
.detail-piggy-leave.detail-piggy-leave-active {
opacity: 0;
}
/* http://www.paulirish.com/2012/box-sizing-border-box-ftw/ */
*, *:before, *:after {
-moz-box-sizing: border-box;

View File

@ -10,7 +10,7 @@
<script src="./deps.js"></script>
<script src="?camli.mode=config&var=CAMLISTORE_CONFIG"></script>
<script type="text/javascript" src="react/react.js"></script>
<script type="text/javascript" src="react/react-with-addons.js"></script>
<!-- Begin non-Closure cheating; but depended on by server_connection.js -->
<script type="text/javascript" src="base64.js"></script>

View File

@ -16,72 +16,92 @@ limitations under the License.
goog.require('camlistore.AnimationLoop');
goog.require('camlistore.ServerConnection');
goog.require('SpritedAnimation');
goog.require('goog.math.Size');
function cached(fn) {
var lastProps;
var lastState;
var lastVal;
return function() {
if (lastState == this.state && lastProps == this.props) {
return lastVal;
}
lastProps = this.props;
lastState = this.state;
lastVal = fn.apply(this, arguments);
return lastVal;
}
}
goog.require('goog.object');
var DetailView = React.createClass({
PREVIEW_MARGIN: 20,
IMG_MARGIN: 20,
PIGGY_WIDTH: 88,
PIGGY_HEIGHT: 62,
getInitialState: function() {
return {
description: null,
};
},
componentDidMount: function(root) {
var imageSize = 100; // We won't use this exact value; we only care about the aspect ratio.
var connection = new camlistore.ServerConnection(this.props.config);
connection.describeWithThumbnails(this.props.blobref, imageSize, function(description) {
this.setState({
this.setState({
description: description
});
}.bind(this));
},
getPreviewSize_: cached(function() {
var meta = this.getPermanodeMeta_();
if (!meta) {
return;
}
var aspect = new goog.math.Size(meta.thumbnailWidth, meta.thumbnailHeight);
var available = new goog.math.Size(
this.props.width - this.getSidebarWidth_() - this.PREVIEW_MARGIN * 2,
this.props.height - this.PREVIEW_MARGIN * 2);
return aspect.scaleToFit(available);
}),
render: function() {
var description = this.state ? this.state.description : '';
this.imgSize_ = this.getImgSize_();
return (
React.DOM.div({className:'detail-view', style: this.getStyle_()},
React.DOM.img({className:'detail-view-preview', key:'preview', src: this.getSrc_(), style: this.getPreviewStyle_()}),
this.getImg_(),
this.getPiggy_(),
React.DOM.div({className:'detail-view-sidebar', key:'sidebar', style: this.getSidebarStyle_()},
React.DOM.pre({key:'sidebar-pre'}, JSON.stringify(description, null, 2)))));
React.DOM.pre({key:'sidebar-pre'}, JSON.stringify(this.state.description || '', null, 2)))));
},
getImg_: function() {
var transition = React.addons.TransitionGroup({transitionName: 'detail-img'}, []);
if (this.state.description) {
transition.props.children.push(
React.DOM.img({
className: 'detail-view-img',
key: 'img',
src: this.getSrc_(),
style: this.getCenteredProps_(this.imgSize_.width, this.imgSize_.height)
})
);
}
return transition;
},
getPiggy_: function() {
var transition = React.addons.TransitionGroup({transitionName: 'detail-piggy'}, []);
if (!this.state.description) {
transition.props.children.push(
SpritedAnimation({
src: 'glitch/npc_piggy__x1_walk_png_1354829432.png',
className: 'detail-view-piggy',
spriteWidth: this.PIGGY_WIDTH,
spriteHeight: this.PIGGY_HEIGHT,
sheetWidth: 8,
sheetHeight: 3,
interval: 30,
style: this.getCenteredProps_(this.PIGGY_WIDTH, this.PIGGY_HEIGHT)
}));
}
return transition;
},
getCenteredProps_: function(w, h) {
var avail = new goog.math.Size(this.props.width - this.getSidebarWidth_(), this.props.height);
return {
top: (avail.height - h) / 2,
left: (avail.width - w) / 2,
width: w,
height: h
}
},
getSrc_: function() {
if (!this.state) {
// TODO(aa): Loading animation
return '';
}
var previewSize = this.getPreviewSize_();
// Only re-request the image if we're increasing in size. Otherwise, let the browser resample.
if (previewSize.height < (this.lastImageHeight || 0)) {
if (this.imgSize_.height < (this.lastImageHeight || 0)) {
console.log('Not re-requesting image becasue new size is smaller than existing...');
} else {
// If we re-request, ask for one bigger than we need right now, so that we're not constantly re-requesting as the browser resizes.
this.lastImageHeight = previewSize.height * 1.25;
this.lastImageHeight = this.imgSize_.height * 1.25;
console.log('Requesting new image with size: ' + this.lastImageHeight);
}
@ -90,6 +110,19 @@ var DetailView = React.createClass({
return uri.toString();
},
getImgSize_: function() {
if (!this.state.description) {
return null;
}
var meta = this.getPermanodeMeta_();
var aspect = new goog.math.Size(meta.thumbnailWidth, meta.thumbnailHeight);
var available = new goog.math.Size(
this.props.width - this.getSidebarWidth_() - this.IMG_MARGIN * 2,
this.props.height - this.IMG_MARGIN * 2);
return aspect.scaleToFit(available);
},
getStyle_: function() {
return {
width: this.props.width,
@ -97,22 +130,6 @@ var DetailView = React.createClass({
}
},
getPreviewStyle_: function() {
if (!this.state || !this.getPreviewSize_().height) {
return {
visibility: 'hidden'
}
}
var avail = new goog.math.Size(this.props.width - this.getSidebarWidth_(), this.props.height);
return {
top: (avail.height - this.getPreviewSize_().height) / 2,
left: (avail.width - this.getPreviewSize_().width) / 2,
width: this.getPreviewSize_().width,
height: this.getPreviewSize_().height
}
},
getSidebarStyle_: function() {
return {
width: this.getSidebarWidth_()
@ -124,9 +141,6 @@ var DetailView = React.createClass({
},
getPermanodeMeta_: function() {
if (!this.state) {
return null;
}
return this.state.description.meta[this.props.blobref];
}
},
});

View File

@ -0,0 +1,11 @@
/**
* Object related utilities beyond what exist in Closure.
*/
goog.provide('object');
function extend(o, n) {
var obj = {};
goog.mixin(obj, o);
goog.mixin(obj, n);
return obj;
}

View File

@ -0,0 +1,28 @@
goog.provide('SpritedAnimation');
goog.require('SpritedImage');
goog.require('object');
var SpritedAnimation = React.createClass({
getInitialState: function() {
return {
index: 0
}
},
componentDidMount: function(root) {
this.timerId_ = window.setInterval(function() {
this.setState({
index: ++this.state.index % (this.props.sheetWidth * this.props.sheetHeight)
})
}.bind(this), this.props.interval);
},
componentWillUnmount: function() {
window.clearInterval(this.timerId_);
},
render: function() {
return SpritedImage(extend(this.props, {index: this.state.index}));
}
});

View File

@ -0,0 +1,27 @@
goog.provide('SpritedImage');
goog.require('goog.object');
goog.require('goog.string');
goog.require('object');
var SpritedImage = React.createClass({
render: function() {
return (
React.DOM.div({className: this.props.className, style: extend(this.props.style, {overflow: 'hidden'})},
React.DOM.img({src: this.props.src, style: this.getImgStyle_()})));
},
getImgStyle_: function() {
var x = this.props.index % this.props.sheetWidth;
var y = Math.floor(this.props.index / this.props.sheetWidth);
if (y >= this.props.sheetHeight) {
throw new Error(goog.string.subs('Index %s out of range', this.props.index));
}
return {
position: 'absolute',
left: -x * this.props.spriteWidth,
top: -y * this.props.spriteHeight
};
}
});

19
third_party/glitch/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
The files in here come from www.glitchthegame.com.
License here: http://www.glitchthegame.com/public-domain-game-art/#licensing
All files are provided by Tiny Speck under the Creative Commons CC0 1.0
Universal License. This is a broadly permissive "No Rights Reserved" license —
you may do what you please with what we've provided. Our intention is to
dedicate these works to the public domain and make them freely available to all,
without restriction. All files are provided AS-IS. Tiny Speck cannot provide any
support to help you bring these assets into your own projects.
Note: the Glitch logo and trademark are not among the things we are making
available under this license. Only items in the files explicitly included herein
are covered.
There is no obligation to link or credit the works, but if you do, please link
to glitchthegame.com, our permanent "retirement" site for the game and these
assets. Of course, links/shoutouts to Tiny Speck (tinyspeck.com) and/or Slack
(slack.com) are appreciated.

24
third_party/glitch/fileembed.go vendored Normal file
View File

@ -0,0 +1,24 @@
/*
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
#fileembed pattern .*\.png
*/
package glitch
import "camlistore.org/pkg/fileembed"
var Files = &fileembed.Files{}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
/*
#fileembed pattern .*react(\.min)?\.js$
#fileembed pattern .*\.js$
*/
package react