add editor viewer

This commit is contained in:
mkwiser
2014-10-19 13:07:14 +08:00
parent e4037f0dbe
commit baa7118c8a
15 changed files with 563 additions and 123 deletions

View File

@@ -1,6 +1,8 @@
@import './bootstrap.less';
@import './def.less';
body, html {
margin: 0;
padding: 0;
@@ -10,14 +12,14 @@ body, html {
// scroll bar
::-webkit-scrollbar {
width: 14px;
height: 14px;
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb {
border-radius: 999px;
border: 5px solid transparent;
border: 1px solid transparent;
}
::-webkit-scrollbar-track {
@@ -70,7 +72,7 @@ body, html {
.sidebar {
position: fixed;
width: 200px;
top: 50px;
top: @top-height;
bottom: 0;
background: #FCFCFC;
padding: 4px;
@@ -79,11 +81,15 @@ body, html {
.main {
margin-left: 200px;
padding: 4px;
padding-top: 50px;
padding: 4px 0 4px 10px;
padding-top: @top-height;
height: 100%;
}
.main.editing {
margin-left: 70%;
}
// loading 动画
.loading {
position: fixed;
@@ -105,6 +111,15 @@ body, html {
}
.editor {
position: fixed;
width: 70%;
top: @top-height;
bottom: 0;
z-index: 1;
background: #FEFEFE;
border-right: 1px solid #DDD;
}
.selection-range {
position: absolute;

5
css/common/def.less Normal file
View File

@@ -0,0 +1,5 @@
// 头部高度
@top-height: 50px;

35
css/common/editor.less Normal file
View File

@@ -0,0 +1,35 @@
// 编辑器样式集合
.glyf-editor {
width: 100%;
height: 100%;
font-size: 12px;
-webkit-user-select: none;
-moz-user-select: none;
.editor-contextmenu {
width: 120px;
border: 1px solid #999;
background: #FEFEFE;
line-height: 24px;
padding: 0 4px;
box-shadow: 1px 1px 1px #CCC;
div {
padding-left: 10px;
cursor: pointer;
border-bottom: 1px solid #CCC;
}
div:hover {
background: #EEE;
}
div:last-child {
border-bottom: none;
}
}
}

View File

@@ -3,7 +3,7 @@
// fontface
@font-face {
font-family: 'fonteditor';
src: url('../../font/iconfont.ttf') format('truetype');
src: url('../font/iconfont.ttf') format('truetype');
}
.i-edit,

6
css/main.less Normal file
View File

@@ -0,0 +1,6 @@
@import './ttf.less';
@import './common/editor.less';

View File

@@ -35,9 +35,8 @@
.glyf-list {
padding-bottom: 1000000px;
margin-bottom: -1000000px;
overflow: hidden;
height: 100%;
overflow: auto;
>.glyf-item {

120
index.html Normal file
View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FontEditor字体编辑器</title>
<link rel="stylesheet" type="text/css" href="./dep/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="./css/main.css">
</head>
<body>
<section class="navbar navbar-inverse" role="navigation">
<button data-action="new" type="button" class="btn btn-primary btn-sm" title="新建ttf字体文件">新建</button>
<button data-action="open" type="button" class="btn btn-primary btn-sm" title="打开ttf,woff格式字体文件">打开</button>
<button data-action="import" type="button" class="btn btn-primary btn-sm" title="导入ttf,woff,svg格式字体文件">导入</button>
<div class="btn-group">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">
插入
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li data-action="add-new"><a>新字形</a></li>
<li data-action="add-online"><a>在线字体</a></li>
</ul>
</div>
<a id="export-btn" href="#" data-action="export" data-type="ttf" class="btn btn-success btn-sm">导出ttf</a>
<a id="export-btn-woff" href="#" data-action="export" data-type="woff" class="btn btn-success btn-sm">导出woff</a>
<a id="export-btn-svg" href="#" data-action="export" data-type="svg" class="btn btn-success btn-sm">导出svg</a>
<button data-action="save" type="button" class="btn btn-warning btn-sm">保存项目</button>
<div class="btn-group">
<button type="button" class="btn btn-info btn-sm dropdown-toggle" data-toggle="dropdown">
设置
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a data-action="setting-unicode">代码点</a></li>
<li><a data-action="setting-name">字体信息</a></li>
<li><a data-action="setting-adjust-pos">调整位置</a></li>
<li><a data-action="setting-adjust-glyf">调整字形</a></li>
<li><a data-action="setting-metrics">调整字体度量</a></li>
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-toggle="dropdown">
预览
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a data-format="ttf" data-action="preview">ttf字体</a></li>
<li><a data-format="woff" data-action="preview">woff字体</a></li>
<li><a data-format="svg" data-action="preview">svg字体</a></li>
</ul>
</div>
</section>
<section class="sidebar">
<div class="project">
<div class="project-title">项目列表</div>
<div id="project-list" class="project-list"></div>
</div>
</section>
<section class="main editing">
<div id="glyf-list" class="glyf-list">
</div>
</section>
<section class="editor" oncontextMenu="return false">
<div id="glyf-editor" class="glyf-editor"></div>
</section>
<div id="selection-range" class="selection-range"></div>
<div id="loading" class="loading"><span>正在加载...</span></div>
<!-- Modal -->
<div class="modal" id="model-dialog" tabindex="-1" role="dialog" aria-labelledby="model-label" aria-hidden="true" data-backdrop="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">关闭</span></button>
<h4 class="modal-title" id="model-label">设置</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary btn-sm btn-confirm">确定</button>
</div>
</div>
</div>
</div>
<script src="./dep/esl.js"></script>
<script src="./dep/jquery.min.js"></script>
<script src="./dep/bootstrap/js/bootstrap.min.js"></script>
<script>
require.config({
baseUrl: './src',
paths: {
deflate: '../dep/pako_deflate.min',
inflate: '../dep/pako_inflate.min',
utpl: '../dep/utpl.min'
}
});
define('jquery', $);
require(['fonteditor/main'])
</script>
<form id="font-form" style="width:0px;height:0px;overflow:hidden;"><input id="font-import" type="file"></form>
</body>
</html>

View File

@@ -143,6 +143,15 @@ define(
return this;
};
/**
* 是否改变过
*
* @return {boolean}
*/
Editor.prototype.isChanged = function() {
return !this.history.atFirst();
};
/**
* 从剪切板中获取
*

View File

@@ -25,23 +25,18 @@ define(
var contours = font.contours;
var originX = this.axis.x;
var originY = this.axis.y;
var scale = this.render.camera.scale;
// 不需要在此保存contours
delete font.contours;
font.rightSideBearing = font.advanceWidth - font.xMax;
this.font = font;
// 重置历史
this.history.reset();
this.history.add(lang.clone(contours));
// 设置字形
var shapes = contours.map(function(contour) {
var shape = {};
pathAdjust(contour, scale, -scale);
shape.points = pathAdjust(contour, 1, 1, originX, originY);
return shape;
return {
points: contour
};
});
var fontLayer = this.fontLayer;
@@ -50,6 +45,18 @@ define(
shapes.forEach(function(shape) {
fontLayer.addShape('path', shape);
});
// 重置历史
this.history.reset();
this.history.add(lang.clone(shapes));
// 设置缩放
var scale = this.render.camera.scale;
shapes.forEach(function(shape) {
pathAdjust(shape.points, scale, -scale);
pathAdjust(shape.points, 1, 1, originX, originY);
});
fontLayer.refresh();
// 设置参考线
@@ -67,7 +74,7 @@ define(
* @return {this}
*/
function setShapes(shapes) {
var origin = this.render.getLayer('axis').shapes[0];
var origin = this.axis;
var scale = this.render.camera.scale;
var fontLayer = this.fontLayer;
@@ -124,14 +131,14 @@ define(
font.name = font.name || '';
var shapes = this.getShapes();
shapes.forEach(function(g) {
var contours = lang.clone(shapes.map(function(shape) {
return shape.points;
})).forEach(function(g) {
pathCeil(g);
});
// 设置边界
var box = computeBoundingBox.computePathBox.apply(null, shapes.map(function(shape) {
return shape.points;
}));
var box = computeBoundingBox.computePathBox.apply(null, contours);
font.xMin = box.x;
font.yMin = box.y;
@@ -141,7 +148,7 @@ define(
font.advanceWidth = font.xMax + font.rightSideBearing;
delete font.advanceWidth;
font.contours = shapes;
font.contours = contours;
return font;
}

117
src/fonteditor/main.js Normal file
View File

@@ -0,0 +1,117 @@
/**
* @file main.js
* @author mengke01
* @date
* @description
* ttf管理器
*/
define(
function(require) {
var GLYFViewer = require('./widget/glyfviewer');
var GLYFEditor = require('./widget/glyfeditor');
var ProjectViewer = require('./widget/projectviewer');
var TTFManager = require('./widget/ttfmanager');
var program = require('./widget/program');
var controller = require('./widget/controller/default');
var actions = require('./widget/actions');
// 打开文件
function onUpFile(e) {
var file = e.target.files[0];
if (program.data.action == 'open' && file.name.match(/(\.ttf|\.woff)$/i)) {
program.loader.load(file, {
type: file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase(),
success: function(imported) {
program.viewer.clearSelected();
program.ttfManager.set(imported);
program.data.projectName = null;
}
});
}
else if (program.data.action == 'import' && file.name.match(/(\.ttf|\.woff|\.svg)$/i)) {
if (program.ttfManager.get()) {
program.loader.load(file, {
type: file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase(),
success: function(imported) {
if (imported.glyf.length) {
program.ttfManager.merge(imported, {scale: true});
}
}
});
}
}
else {
alert('无法识别文件类型!');
}
e.target.value = '';
}
// 绑定组件
function bindEvent() {
$('.navbar').delegate('[data-action]', 'click', function(e) {
var action = $(this).attr('data-action');
if (actions[action]) {
program.data.action = action;
actions[action].call(this, e);
}
});
$('#export-btn').on('mouseup', actions['export-file']);
$('#export-btn-woff').on('mouseup', actions['export-file']);
$('#export-btn-svg').on('mouseup', actions['export-file']);
document.getElementById('font-import').addEventListener('change', onUpFile);
}
var entry = {
/**
* 初始化
*/
init: function () {
bindEvent();
// glyf查看器
program.viewer = new GLYFViewer($('#glyf-list'));
// 字体查看器
program.editor = new GLYFEditor($('#glyf-editor'));
// 项目管理
program.project = require('./widget/project');
program.projectViewer = new ProjectViewer($('#project-list'));
// ttf管理
program.ttfManager = new TTFManager();
// 导入导出器
program.loader = require('./widget/loader');
program.exporter = require('./widget/exporter');
// 预览器
program.previewer = require('./widget/previewer');
controller.init(program);
// 加载项目
program.projectViewer.show(program.project.items());
// for test
$.getJSON('./demo/js/baiduHealth.json', function(ttf) {
program.ttfManager.set(ttf);
program.viewer.focus();
});
}
};
entry.init();
return entry;
}
);

View File

@@ -9,7 +9,7 @@
define(
function(require) {
var lang = require('common/lang');
var clipboard = require('../clipboard');
var string = require('common/string');
@@ -21,10 +21,22 @@ define(
* @param {Object} program 项目组件
*/
init: function(program) {
program.viewer.on('del', function(e) {
if (e.list) {
program.ttfManager.removeGlyf(e.list);
}
}).on('edit', function(e) {
$('.main').addClass('editing');
$('.editor').addClass('editing');
var list = program.ttfManager.getGlyf(e.list);
if (list[0].compound) {
alert('暂不支持复合字形!');
}
else {
program.editor.show(lang.clone(list[0]));
}
}).on('copy', function(e) {
var list = program.ttfManager.getGlyf(e.list);
clipboard.set(list, 'glyf');
@@ -32,37 +44,10 @@ define(
var list = program.ttfManager.getGlyf(e.list);
clipboard.set(list, 'glyf');
program.ttfManager.removeGlyf(e.list);
}).on('paste', function(e) {
var glyfList = clipboard.get('glyf');
if (glyfList && glyfList.length) {
program.ttfManager.appendGlyf(glyfList, program.viewer.getSelected());
}
}).on('undo', function(e) {
program.ttfManager.undo();
}).on('redo', function(e) {
program.ttfManager.redo();
}).on('save', function(e) {
// 保存项目
if (program.ttfManager.get()) {
if (program.data.projectName) {
program.project.add(program.data.projectName, program.ttfManager.get());
program.ttfManager.setState('new');
program.loading.show('保存成功..', 400);
}
else {
var name = '';
if ((name = window.prompt('请输入项目名称:'))) {
name = string.encodeHTML(name);
var list = program.project.add(name, program.ttfManager.get());
program.projectViewer.show(list);
program.data.projectName = name;
program.ttfManager.setState('new');
program.loading.show('保存成功..', 400);
}
}
}
});
program.projectViewer.on('open', function(e) {
@@ -87,6 +72,39 @@ define(
program.viewer.focus();
});
program.on('save', function(e) {
// 保存项目
if (program.ttfManager.get()) {
if (program.data.projectName) {
program.project.add(program.data.projectName, program.ttfManager.get());
program.ttfManager.setState('new');
program.loading.show('保存成功..', 400);
}
else {
var name = '';
if ((name = window.prompt('请输入项目名称:'))) {
name = string.encodeHTML(name);
var list = program.project.add(name, program.ttfManager.get());
program.projectViewer.show(list);
program.data.projectName = name;
program.ttfManager.setState('new');
program.loading.show('保存成功..', 400);
}
}
}
}).on('paste', function(e) {
var glyfList = clipboard.get('glyf');
if (glyfList && glyfList.length) {
program.ttfManager.appendGlyf(glyfList, program.viewer.getSelected());
}
});
program.init({
viewer: program.viewer.main.get(0),
editor: program.editor.main.get(0)
});
window.onbeforeunload = function() {
if (program.ttfManager.isChanged()) {
return '是否放弃保存当前项目?';

View File

@@ -0,0 +1,59 @@
/**
* @file GLYFEditor.js
* @author mengke01
* @date
* @description
* 编辑器组件
*/
define(
function(require) {
var editor = require('editor/main');
/**
* font查看器
*
* @constructor
* @param {HTMLElement} main 主元素
* @param {Object} options 参数
*/
function GLYFEditor(main, options) {
this.main = $(main);
this.options = options || {};
}
GLYFEditor.prototype.show = function(font) {
if (!this.editor) {
this.editor = editor.create(this.main.get(0));
}
this.main.show();
if (font) {
this.editor.setFont(font);
}
this.editor.focus();
this.editing = true;
};
GLYFEditor.prototype.hide = function() {
this.editor.blur();
this.main.hide();
this.editing = false;
};
GLYFEditor.prototype.isEditing = function() {
return this.editing;
};
// 导出editor的函数
['focus', 'blur', 'setFont', 'getFont', 'setShapes', 'getShapes'].forEach(function(fn) {
GLYFEditor.prototype[fn] = function() {
return this.editor ? this.editor[fn].apply(this.editor, arguments) : undefined;
};
});
return GLYFEditor;
}
);

View File

@@ -1,5 +1,5 @@
/**
* @file glyflist.js
* @file GLYFViewer.js
* @author mengke01
* @date
* @description
@@ -15,6 +15,7 @@ define(
var GLYF_ITEM_TPL = ''
+ '<div data-index="${index}" class="glyf-item ${compound} ${modify} ${selected}">'
+ '<i data-action="edit" class="i-edit" title="编辑"></i>'
+ '<i data-action="del" class="i-del" title="删除"></i>'
+ '<svg class="glyf" viewbox="0 0 ${unitsPerEm} ${unitsPerEm}">'
+ '<g transform="scale(1, -1) translate(0, -${descent}) scale(0.95, 0.95) ">'
@@ -27,8 +28,7 @@ define(
var keyMap = {
46: 'del',
67: 'copy',
88: 'cut',
86: 'paste'
88: 'cut'
};
// 显示glyf
@@ -120,62 +120,47 @@ define(
function downlistener(e) {
var me = this;
if (e.keyCode >= 112 && e.keyCode <= 119 && e.keyCode !== 116) {
e.preventDefault();
e.stopPropagation();
me.fire('function', {
keyCode: e.keyCode
});
}
// 保存
if (83 === e.keyCode && e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
me.fire('save');
}
// 处理其他事件,需要focus
else {
if (me.listening) {
// 阻止ctrl+A默认事件
if (65 === e.keyCode && e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
me.main.children().addClass('selected');
}
// 撤销
else if(e.keyCode == 90 && e.ctrlKey) {
e.stopPropagation();
me.fire('undo');
}
// 重做
else if(e.keyCode == 89 && e.ctrlKey) {
e.stopPropagation();
me.fire('redo');
}
// 取消选中
else if (27 === e.keyCode) {
e.stopPropagation();
if (me.listening) {
// 阻止ctrl+A默认事件
if (65 === e.keyCode && e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
me.main.children().addClass('selected');
}
// 撤销
else if(e.keyCode == 90 && e.ctrlKey) {
e.stopPropagation();
me.fire('undo');
}
// 重做
else if(e.keyCode == 89 && e.ctrlKey) {
e.stopPropagation();
me.fire('redo');
}
// 取消选中
else if (27 === e.keyCode) {
e.stopPropagation();
me.clearSelected();
}
// 其他操作
else if (keyMap[e.keyCode] && (e.keyCode == 46 || e.ctrlKey)) {
e.stopPropagation();
var selected = me.getSelected();
// 取消选中的glyf
if (e.keyCode == 46 || e.keyCode == 88) {
me.clearSelected();
}
// 其他操作
else if (keyMap[e.keyCode] && (e.keyCode == 46 || e.ctrlKey)) {
e.stopPropagation();
var selected = me.getSelected();
// 取消选中的glyf
if (e.keyCode == 46 || e.keyCode == 88) {
me.clearSelected();
}
// 粘贴和有选择的操作需要发事件
if (e.keyCode == 86 || selected.length) {
me.fire(keyMap[e.keyCode], {
list: selected
});
}
// 粘贴和有选择的操作需要发事件
if (selected.length) {
me.fire(keyMap[e.keyCode], {
list: selected
});
}
}
}
}
/**
@@ -185,7 +170,7 @@ define(
* @param {HTMLElement} main 主元素
* @param {Object} options 参数
*/
function GlyfViewer(main, options) {
function GLYFViewer(main, options) {
this.options = options || {};
this.main = $(main);
@@ -196,11 +181,6 @@ define(
var me = this;
me.downlistener = lang.bind(downlistener, this);
$(document.body).on('click', function(e) {
var focused = me.main.get(0) === e.target || me.main.get(0).contains(e.target);
focused ? me.focus() : me.blur();
});
me.capture = new MouseCapture(me.main.get(0), {
events: {
dblclick: false,
@@ -249,7 +229,7 @@ define(
/**
* 获取焦点
*/
GlyfViewer.prototype.focus = function() {
GLYFViewer.prototype.focus = function() {
if (!this.listening) {
this.listening = true;
document.body.addEventListener('keydown', this.downlistener);
@@ -259,10 +239,10 @@ define(
/**
* 失去焦点
*/
GlyfViewer.prototype.blur = function() {
GLYFViewer.prototype.blur = function() {
if (this.listening) {
this.listening = false;
document.body.addEventListener('keydown', this.downlistener);
document.body.removeEventListener('keydown', this.downlistener);
}
};
@@ -272,7 +252,7 @@ define(
* @param {Object} ttf ttfObject
* @param {Array?} selectedList 选中的列表
*/
GlyfViewer.prototype.show = function(ttf, selectedList) {
GLYFViewer.prototype.show = function(ttf, selectedList) {
showGLYF.call(this, ttf, selectedList);
};
@@ -281,7 +261,7 @@ define(
*
* @return {Array} 选中的indexList
*/
GlyfViewer.prototype.getSelected = function() {
GLYFViewer.prototype.getSelected = function() {
var selected = [];
this.main.find('.selected').each(function(index, item) {
selected.push(+item.getAttribute('data-index'));
@@ -292,14 +272,14 @@ define(
/**
* 清除选中列表
*/
GlyfViewer.prototype.clearSelected = function() {
GLYFViewer.prototype.clearSelected = function() {
this.main.children().removeClass('selected');
};
/**
* 设置选中的列表
*/
GlyfViewer.prototype.setSelected = function(selectedList) {
GLYFViewer.prototype.setSelected = function(selectedList) {
if (selectedList && 0 !== selectedList.length) {
var selectedHash = {};
@@ -315,8 +295,8 @@ define(
}
};
require('common/observable').mixin(GlyfViewer.prototype);
require('common/observable').mixin(GLYFViewer.prototype);
return GlyfViewer;
return GLYFViewer;
}
);

View File

@@ -10,8 +10,76 @@
define(
function(require) {
var observable = require('common/observable');
// 绑定click事件
function bindClick(components) {
var me = this;
document.body.addEventListener('click', function(e) {
// 监听查看器
if (components.viewer === e.target || components.viewer.contains(e.target)) {
me.viewer.focus();
}
else {
me.viewer.blur();
}
// 监听编辑器
if (components.editor) {
if (components.editor === e.target || components.editor.contains(e.target)) {
me.editor.focus();
}
else {
me.editor.blur();
}
}
}, false);
}
/**
* 绑定键盘事件
*/
function bindKey() {
var me = this;
document.body.addEventListener('keydown', function(e) {
// 功能键
if (e.keyCode >= 112 && e.keyCode <= 119 && e.keyCode !== 116) {
e.preventDefault();
e.stopPropagation();
me.fire('function', {
keyCode: e.keyCode
});
}
// 保存
else if (83 === e.keyCode && e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
me.fire('save');
}
// 粘贴
else if ((e.keyCode == 86 && e.ctrlKey)) {
e.preventDefault();
e.stopPropagation();
me.fire('paste');
}
});
}
var program = {
/**
* 初始化
*/
init: function(components) {
bindClick.call(this, components);
bindKey.call(this, components);
},
/**
* 暂存对象
*
@@ -30,6 +98,8 @@ define(
loading: require('./loading')
};
observable.mixin(program);
return program;
}
);

View File

@@ -208,7 +208,7 @@ define(
*
*/
Render.prototype.focus = function() {
this.capture.start();
//this.capture.start();
this.keyCapture.start();
};
@@ -217,7 +217,7 @@ define(
*
*/
Render.prototype.blur = function() {
this.capture.stop();
//this.capture.stop();
this.keyCapture.stop();
};