Port character page to Meteor

This commit is contained in:
Shaunak Kishore
2016-02-07 16:15:07 -05:00
parent 51920f466e
commit 0e5419373a
7 changed files with 195 additions and 165 deletions

View File

@ -1,72 +0,0 @@
<script type="text/ng-template" id="decomposition.template">
<div class="decomposition">
<div class="header"><div class="value">{{node.value}}</div> -</div>
<div class="text">{{node.label}}</div>
<ng-include src="'decomposition.template'"
ng-repeat="node in node.children"></ng-include>
</div>
</script>
<script type="text/ng-template" id="order.template">
<svg viewBox="0 0 1024 1024">
<g stroke="lightgray" stroke-dasharray="1,1" stroke-width="1"
transform="scale(4, 4)">
<line x1="0" y1="0" x2="256" y2="256"></line>
<line x1="256" y1="0" x2="0" y2="256"></line>
<line x1="128" y1="0" x2="128" y2="256"></line>
<line x1="0" y1="128" x2="256" y2="128"></line>
</g>
<g transform="scale(1, -1) translate(0, -900)">
<path ng-repeat="stroke in controller.strokes"
x-ng-attr-class="{{stroke.class}}"
x-ng-attr-d="{{stroke.d}}"></path>
<g ng-repeat="animation in controller.animations">
<clipPath id="{{animation.clip}}">
<path x-ng-attr-d="{{animation.stroke}}"></path>
</clipPath>
<path x-ng-attr-class="animation {{animation.class}}"
x-ng-attr-clip-path="url(#{{animation.clip}})"
x-ng-attr-d="{{animation.median}}"
x-ng-attr-stroke-dasharray="
{{animation.length}} {{animation.spacing}}"
x-ng-attr-stroke-dashoffset="{{animation.advance}}"/>
</g>
</g>
</svg>
</script>
<div id="data" ng-controller="DataController as controller"
class="{{controller.short}} {{controller.orientation}}">
<div class="pane" ng-if="controller.orientation === 'vertical'">
<ng-include src="'order.template'"></ng-include>
</div>
<div class="pane">
<div class="panel panel-primary metadata">
<div class="panel-heading">
<h3 class="panel-title">
Metadata for {{controller.character}}
</h3>
</div>
<div class="panel-body">
<div class="field" ng-repeat="metadata in controller.metadata">
<label class="control-label">{{metadata.label}}</label>
<div class="value">{{metadata.value}}</div>
</div>
</div>
</div>
<div class="panel panel-primary metadata">
<div class="panel-heading">
<h3 class="panel-title">
Decomposition for {{controller.character}}
</h3>
</div>
<div class="panel-body">
<ng-include src="'decomposition.template'"
ng-repeat="node in controller.decomposition"></ng-include>
</div>
</div>
</div>
<div class="pane" ng-if="controller.orientation === 'horizontal'">
<ng-include src="'order.template'"></ng-include>
</div>
</div>

View File

@ -0,0 +1,13 @@
// Simple helpers for interacting with reactive variables.
ReactiveVar.prototype.pop = function() {
const value = this.get();
value.pop();
this.set(value);
}
ReactiveVar.prototype.push = function(element) {
const value = this.get();
value.push(element);
this.set(value);
}

View File

@ -0,0 +1,72 @@
<template name="decomposition">
<div class="decomposition">
<div class="header"><div class="value">{{value}}</div> -</div>
<div class="text">{{label}}</div>
{{#each children}}
{{> decomposition this}}
{{/each}}
</div>
</template>
<template name="order">
<svg viewBox="0 0 1024 1024">
<g stroke="lightgray" stroke-dasharray="1,1" stroke-width="1"
transform="scale(4, 4)">
<line x1="0" y1="0" x2="256" y2="256"></line>
<line x1="256" y1="0" x2="0" y2="256"></line>
<line x1="128" y1="0" x2="128" y2="256"></line>
<line x1="0" y1="128" x2="256" y2="128"></line>
</g>
<g transform="scale(1, -1) translate(0, -900)">
{{#each strokes}}
<path class="{{class}}" d="{{d}}"></path>
{{/each}}
{{#each animations}}
<clipPath id="{{clip}}">
<path d="{{stroke}}"></path>
</clipPath>
<path class="animation {{class}}" clip-path="url(#{{clip}})"
d="{{median}}" stroke-dasharray="{{length}} {{spacing}}"
stroke-dashoffset="{{advance}}"></path>
{{/each}}
</g>
</svg>
</template>
<template name="character">
<div id="character" class="{{short}} {{orientation}}">
{{#if vertical}}
<div class="pane">{{> order}}</div>
{{/if}}
<div class="pane">
<div class="panel panel-primary metadata">
<div class="panel-heading">
<h3 class="panel-title">
Metadata for {{character}}
</h3>
</div>
<div class="panel-body">
{{#each metadata}}
<div class="field">
<label class="control-label">{{format label}}</label>
<div class="value">{{value}}</div>
</div>
{{/each}}
</div>
</div>
<div class="panel panel-primary metadata">
<div class="panel-heading">
<h3 class="panel-title">
Decomposition for {{character}}
</h3>
</div>
<div class="panel-body">
{{> decomposition tree}}
</div>
</div>
</div>
{{#if horizontal}}
<div class="pane">{{> order}}</div>
{{/if}}
</div>
</template>

View File

@ -1,12 +1,20 @@
"use strict";
const animations = new ReactiveVar();
const character = new ReactiveVar();
const metadata = new ReactiveVar();
const strokes = new ReactiveVar();
const tree = new ReactiveVar();
const animate = window.requestAnimationFrame ||
((callback) => setTimeout(callback, 1000 / 60));
let animation = null;
const orientation = new ReactiveVar('horizontal');
const short = new ReactiveVar(false);
// Methods used to render all the various pieces of character metadata.
const augmentTreeWithLabels = (node, dependencies) => {
const value = node.value;
if (node.type === 'compound') {
node.label = lower(decomposition_util.ids_data[value].label);
node.label = lower(makemeahanzi.Decomposition.ids_data[value].label);
node.children.map((child) => augmentTreeWithLabels(child, dependencies));
} else {
node.label = dependencies[node.value] || '(unknown)';
@ -14,8 +22,8 @@ const augmentTreeWithLabels = (node, dependencies) => {
}
const constructTree = (row) => {
const decomposition = row.decomposition;
const tree = decomposition_util.convertDecompositionToTree(decomposition);
const util = makemeahanzi.Decomposition;
const tree = util.convertDecompositionToTree(row.decomposition);
augmentTreeWithLabels(tree, row.dependencies);
return tree;
}
@ -45,74 +53,97 @@ const lower = (string) => {
return string[0].toLowerCase() + string.substr(1);
}
const DataController = function($scope, $routeParams, $http) {
this.character = String.fromCharCode(parseInt($routeParams.codepoint, 10));
this.animations = [];
this.decomposition = [];
this.metadata = [];
this.strokes = [];
this._animation = null;
this._advanceAnimation = () => {
if (!this._animation) {
return;
}
const step = this._animation.step();
$scope.$apply(() => {
const num_complete = step.animations.length - (step.complete ? 0 : 1);
for (let i = 0; i < num_complete; i++) {
this.strokes[i].class = 'complete';
}
this.animations = step.animations.slice(num_complete);
});
if (!step.complete) {
animate(this._advanceAnimation);
}
}
this._getCharacterData = (character, callback) => {
const part = Math.floor(character.charCodeAt(0) / 256);
$http.get(`data/part-${part}.txt`).then((response) => {
const data = response.data;
for (let row of response.data) {
if (row.character === character) {
return callback(row);
}
}
const refreshMetadata = (row) => {
const options = {delay: 0.3, speed: 0.02};
animation = new makemeahanzi.Animation(options, row.strokes, row.medians);
animate(advanceAnimation);
metadata.set([
{label: 'Definition:', value: row.definition},
{label: 'Pinyin:', value: row.pinyin.join(', ')},
{label: 'Radical:', value: row.radical},
]);
if (row.etymology) {
metadata.push({
label: 'Formation:',
value: formatEtymology(row.etymology),
});
}
strokes.set(row.strokes.map((d) => ({d: d, class: 'incomplete'})));
tree.set(constructTree(row));
}
this._refresh = (row) => {
const short = window.innerWidth <= 480;
this.decomposition.push(constructTree(row));
this.metadata = [
{label: (short ? 'Def.' : 'Definition:'), value: row.definition},
{label: (short ? 'Pin.' : 'Pinyin:'), value: row.pinyin.join(', ')},
{label: (short ? 'Rad.' : 'Radical:'), value: row.radical},
];
if (row.etymology) {
this.metadata.push({
label: (short ? 'For.' : 'Formation:'),
value: formatEtymology(row.etymology),
});
const updateCharacter = () => {
animation = null;
[animations, metadata, strokes, tree].map((x) => x.set(null));
const value = character.get();
if (value == null) {
return;
}
const part = Math.floor(value.charCodeAt(0) / 256);
$.get(`characters/part-${part}.txt`, (response, code) => {
if (code !== 'success') throw new Error(code);
const data = JSON.parse(response);
for (let row of data) {
if (row.character === character.get()) {
refreshMetadata(row);
}
}
this.strokes = row.strokes.map((d) => ({d: d, class: 'incomplete'}));;
const options = {delay: 0.3, speed: 0.02};
this._animation = new Animation(options, row.strokes, row.medians);
animate(this._advanceAnimation);
}
this._resize = () => {
this.short = window.innerWidth <= 480 ? 'short ' : '';
this.orientation = window.innerWidth < window.innerHeight ?
'vertical' : 'horizontal';
}
this._getCharacterData(this.character, this._refresh);
this._resize();
$scope.$on('$destroy', (event) => {
this._animation = null;
});
}
// Methods for running the stroke-order animation.
const animate = window.requestAnimationFrame ||
((callback) => setTimeout(callback, 1000 / 60));
const advanceAnimation = () => {
if (animation == null) {
return;
}
const step = animation.step();
const complete = step.animations.length - (step.complete ? 0 : 1);
if (complete > 0 && strokes.get()[complete - 1].class !== 'complete') {
const current = strokes.get();
for (let i = 0; i < complete ; i++) {
current[i].class = 'complete';
}
strokes.set(current);
}
animations.set(step.animations.slice(complete));
if (!step.complete) {
animate(advanceAnimation);
}
}
const resize = () => {
short.set(window.innerWidth <= 480 ? 'short ' : '');
orientation.set(window.innerWidth < window.innerHeight ?
'vertical' : 'horizontal');
}
// Meteor template bindings.
Template.character.helpers({
character: () => character.get(),
metadata: () => metadata.get(),
tree: () => tree.get(),
orientation: () => orientation.get(),
short: () => short.get(),
format: (label) => (short.get() ? label.slice(0, 3) + ':' : label),
horizontal: () => orientation.get() === 'horizontal',
vertical: () => orientation.get() === 'vertical',
});
Template.order.helpers({
animations: () => animations.get(),
strokes: () => strokes.get(),
});
Meteor.startup(() => {
character.set('职');
Deps.autorun(updateCharacter)
resize();
});

View File

@ -14,5 +14,5 @@
</div>
</div>
</div>
{{> search}}
{{> character}}
</body>

View File

@ -10,20 +10,6 @@ makemeahanzi.mediansPromise.then((medians) => {
Deps.autorun(refreshCandidates);
}).catch(console.error.bind(console));
// Simple helpers for interacting with reactive variables.
const pop = (variable) => {
const value = variable.get();
value.pop();
variable.set(value);
}
const push = (variable, element) => {
const value = variable.get();
value.push(element);
variable.set(value);
}
// Methods needed to initialize the drawing canvas.
const createSketch = function() {
@ -91,8 +77,8 @@ const d = (path) => {
const endStroke = () => {
const new_path = d(stroke.get());
if (new_path.length > 0) {
push(paths, new_path);
push(strokes, stroke.get());
paths.push(new_path);
strokes.push(stroke.get());
}
stroke.set([]);
}
@ -110,13 +96,13 @@ const pushPoint = (point) => {
}
const refreshCandidates = () => {
const data = strokes.get();
candidates.set(data.length > 0 ? matcher.match(data, 8) : []);
const value = strokes.get();
candidates.set(value.length > 0 ? matcher.match(value, 8) : []);
}
const undo = () => {
pop(paths);
pop(strokes);
paths.pop();
strokes.pop();
stroke.set([]);
}

View File

@ -39,7 +39,7 @@ body {
}
}
#data {
#character {
margin-top: 60px;
.pane {