Welcome to my blog. You can post whatever you like!

The blog post is using markdown, and here is a simple guide. Markdown Guide

LOGIN TO POST A BLOG

基于Django国际化的多语言实现

Satori

Author: Satori

Posted: 2018-02-14 21:32:57

Category: Django HTML JavaScript 技术

Views: 4401 Comments: 9

## 一、背景 做国际化是非常重要的事情。之前网站做出来的时候母亲表示全是英语看不懂。当时我就被触动了一下,虽然现阶段网站根本没有什么流量,但是大部分用户还是国内的,因此不做中文化翻译其他人很难去使用,而且做出国际化之后真的是让自己的网站一下子高大上起来。因此我决定还是做一做国际化。 ## 二、实现方法 国际化的困难程度在我做之前就有估计,不过真实做的时候发现真的比较复杂。复杂不是在于实现难度,而是在细碎程度。因为**HTML文本**,**Javascript代码**,**Django 模型表单标签**都需要进行汉化工作。要做到疏而不漏很困难。还有就是歧义问题,同一个词在不同地方意思不同,还需要自己去做区分。 具体的实现方法参考[官方文档][1],主题思路就是通过在需要翻译的地方进行标注,然后使用Django命令将要翻译的地方生成一个翻译文件,接着补全翻译文件,最后再用Django命令生成翻译的二进制文件即可。 另外一点是需要切换语言,这可以用内置的方法往某个地址post数据即可更改。 ## 三、具体步骤 ### 1. 修改setting.py使之支持国际化 在`MIDDLEWARE`中加一个`'django.middleware.locale.LocaleMiddleware',`,这个中间件负责国际化的语言转化等等。 然后需要加一个`LANGUAGE_CODE = 'zh-hans'`,默认使用中文,再添加语言种类,这里我就添加英语和中文,如下。 ``` LANGUAGES = [ ('en', _('English')), ('zh-hans', _('Chinese')), ] ``` 最后需要定义翻译的文件放在哪里,我们直接放在locale文件夹中,代码如下。 ``` LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale'), ) ``` ### 2. HTML文本的国际化 HTML文本的国际化相对比较简单,有两种方式来标记需要翻译的部分,第一种是直接将要翻译的文本放到`{% trans "xxx" %}`中,比如,`{% trans "language" %}`。 第二种方式适合大段文本的翻译,将要翻译的内容包含在`{% blocktrans %}`和`{% endblocktrans %}`中,比如。 ``` {% blocktrans %} Hello everyone {% endblocktrans %} ``` 但是需要注意的是当要翻译的文本中有变量之类的时候,只能用第二种方式,而且需要给变量一个别名,然后在翻译的文本中去引用,比如有原文是 ``` title: {{ blog.title }}, author: {{ blog.author }} ``` 需要声明一个变量储存,代码如下。 ``` {% blocktrans with title=blog.title author=blog.author %} title: {{ title }}, author: {{ author }} {% endblocktrans %} ``` 多个变量就往后加就行了,如上例。 其次遇到url之类的也需要提前存储,比如原文是 ``` <a href="{% url 'auth_login' %}">Login</a> ``` 需要有一个别名代替url,代码如下。 ``` {% url 'auth_login' as login_url %} {% blocktrans %} <a href="{{ login_url }}">Login</a> {% endblocktrans %} ``` 最后需要注意歧义问题,对于template的歧义,加上context字段,例子如下。 ``` {% blocktrans context "month" %} He will leave on May 2nd. {% endblocktrans %} ``` ### 3. Django源文件的国际化 这部分国际化也比较简单,使用内置的translation中的gettext一系列方法即可获取文本。首先在python文件头部进行声明ugettext以及ugettext_lazy等方法,然后在需要翻译的地方用该方法包装即可。但是需要注意的是`ugettext_lazy`方法只能用于Model以及Form字段的翻译,不能直接作为HttpResponse的返回,这时候需要`ugettext`方法。例子如下。 ``` from django.utils.translation import ugettext as _ def index(request): context = {} blog_list = Blog.objects.all().order_by('-publish_time') page = request.GET.get('page') blogs, page_list = BlogService.get_paginated_items(blog_list, page) context['blogs'] = blogs context['page_list'] = page_list context['title'] = _("Blogs") return render(request, "blogs/index.html", context) ``` 这样就能返回多语言结果了。对于`ugettext_lazy`方法的例子如下。 ``` from django.utils.translation import ugettext_lazy as _ class ProfileForm(ModelForm): class Meta: model = Profile fields = ['gender', 'birthday', 'mobile', 'residence', 'website', 'microblog', 'qq', 'wechat', 'introduction'] labels = { 'gender': _('gender'), 'birthday': _('birthday'), 'mobile': _('mobile'), 'residence': _('residence'), 'website': _('website'), 'microblog': _('microblog'), 'qq': _('QQ'), 'wechat': _('wechat'), 'introduction': _('introduction'), } ``` 对于有上下文才能翻译的词句,需要用`pgettext`方法,例子如下。 ``` from django.utils.translation import pgettext month = pgettext("month name", "May") ``` 这表示"May"这个词的上下文是"month",也就是月份,之后就好分辩怎么翻译,同样也不会和其他may文字冲突。 最后再把template tag中返回给前端的数据做国际化即可。 ### 4. JavaScript的国际化 Javascript的国际化本身比较麻烦,因为它不能直接读翻译文件,也没有直接的翻译函数。还好Django帮我们做了这些事情。首先在根urls.py中引入i18n的url,代码如下。 ``` from django.views.i18n import JavaScriptCatalog # other code urlpatterns += [ url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'), ] ``` 之后只需在引入javascript的html中引入django内置的一个javascript文件,将它引入到其他javascript文件之前即可。代码如下。 ``` <script type="text/javascript" src="{% url 'javascript-catalog' %}"></script> ``` 然后我们就可以在之后的javascript代码直接使用和之前类似python中的`ugettext`,`ugettext_lazy`, `pgettext`函数了。业务代码如下。 ``` document.write(ugettext('this is to be translated')); ``` ### 5. 生成翻译文件以及编译翻译文件 在前面的步骤当中已经将python代码和HTML代码和Javascript代码中需要翻译的地方全部标记了,下一步就是生成翻译文件并进行翻译。首先需要建一个locale文件夹,直接`mkdir locale`即可。下面执行得到翻译文件的命令。加上`-l`标签表示需要翻译的目标语言,比如得到中文翻译文件。 ``` django-admin makemessages -l zh-Hans ``` 对于javascript文件还需要单独执行一下命令。 ``` django-admin makemessages -d djangojs -l zh_Hans ``` 执行之后,就会得到两个翻译文件,`django.po`和`djangojs.po`。每个文件的条目大致如下。 ``` #: accounts/forms.py:11 msgid "gender" msgstr "性别" ``` 第一行注释是出现的位置,第二行是msgid,也就是原文,这个不能修改,第三行是msgstr,就是需要翻译的文本,如果有上下文,就会有`msgctxt`字段,这表示这段文字的上下文。当全部翻译完成之后,再执行命令得到翻译后的二进制文件。命令如下。 ``` django-admin compilemessages ``` 然后就会得到两个.mo文件,这就是翻译后的文件 。 ### 6. 切换语言 对于语言的切换,需要创建一个表单,然后向Django内置的setlang的url发带有`language`字段的数据即可,采用 POST方法。示例代码如下。 ``` <div class="lang-div dropdown-menu" role="menu"> {% get_current_language as LANGUAGE_CODE %} {% get_available_languages as LANGUAGES %} {% get_language_info_list for LANGUAGES as languages %} {% for language in languages %} <a class="lang-item btn {% if language.code == LANGUAGE_CODE %}btn-primary{% else %}btn-default{% endif %}" onclick="$('#lang_type').val('{{ language.code }}'); $('#lang_form').submit();"> {{ language.name_local }} </a> {% endfor %} </div> <form action="{% url 'set_language' %}" method="post" id="lang_form">{% csrf_token %} <input name="language" id="lang_type" type="hidden"> <input name="next" type="hidden" value="{{ redirect_to }}" /> </form> ``` 这种方法通过按钮触发向input元素传入值,再发送即可实现语言切换。 ## 四、总结 总之做国际化还是非常繁琐了,需要各种找,各种翻译,需要几天才能真正开发完。 [1]: https://docs.djangoproject.com/en/1.10/topics/i18n/translation/

Read More

和谐图片浏览以及实现方法

Satori

Author: Satori

Posted: 2018-02-11 22:33:02

Category: CSS Django HTML JavaScript 技术

Views: 2011 Comments: 0

## 一、开启关闭方法 在非文本框区域依次按键盘 **`左`** **`下`** **`右`** **`上`** 即可开启。开启后,再按同样按键退出。 ## 二、背景 这次的设计主要想法还是来自[忧郁的弟弟][1]网站的设计。他的设计方法是修改用户右键的点击来实现和谐模式的开启。然后和谐模式开启之后,鼠标的范围就会出现一个圈,和谐图片就只显示在圈内,然后通过移动鼠标来实现看图片的不同部分,然后再按鼠标即可退出。 ## 三、实现方法 这次的实现方法还是比较简单。首先后端传和谐图片相关的html代码,中间就包含和谐图片链接。具体实现方案还是按照Django的template tag方式实现,跟之前的背景图片相似。然后前端监听触发和谐模式开启按键,然后再监听用户鼠标移动事件来调整圈的位置以及圈内图片的位置。 ## 四、具体步骤 1. 后端返回和谐模块html 这个部分依然使用template tag实现,后端直接返回html代码。先调用系统接口获取和谐图片的个数以及种类,任意挑一个,把它的url塞到html里img的src中即可。这部分代码拷贝之前的,具体如下。 ``` @register.simple_tag def load_hidden_background(): image_show_count = 1 curdir = os.path.join(settings.MEDIA_ROOT, "background", "H") if os.path.exists(curdir): items = os.listdir(curdir) if len(items) >= image_show_count: selected = [items[i] for i in random.sample(range(len(items)), image_show_count)] urls = [settings.MEDIA_URL + "background/H/{}".format(x) for x in selected] return mark_safe(""" <div class="hidden-bg"> <div class="circle" style="background: url({}) no-repeat"></div> </div> """.format(*urls)) ``` 然后之后的template只需在中间加一个tag就行, ``` {% load_hidden_background %} ``` 这里还是聊一下返回的html,先有一个大的div叫hidden-bg,它填充整个页面,并置于页面最顶层,背景是灰色,目的是让之后的图片更醒目,初始状态不显示。hidden-div中嵌套新的div叫circle,这个就是圈,图片就在圈里面显示,图片显示方式是通过backgroud属性设置。圈通过border-radius使方块变圈。 同时附上这两个div的css。 ``` @media (min-width: 767px) { .hidden-bg { cursor: url(/static/mysite/images/mouse_none.cur), url(/static/mysite/images/mouse_none.cur), auto; } } .circle { width: 200px; height: 200px; position: fixed; border-radius: 100%; box-shadow: 0 0 0 3px rgba(255,255,255,0.85), 0 0 7px 7px rgba(0,0,0,0.25), inset 0 0 40px 2px rgba(0,0,0,0.25); } .hidden-bg { position: fixed; width: 100%; height: 100%; max-width: 100%; max-height: 100%; left: 0px; top: 0px; display: none; z-index: 1000001; opacity: 1; background: rgba(0, 0, 0, 0.8); } ``` 2. 前端监听按键触发 这个部分和之前讲过的完全一样,绑定一个按键组合即可。 ``` key_listener.sequence_combo("left down right up", toggle_hidden_background, true); ``` 3. 前端处理鼠标移动 这个部分是核心部分。要明确鼠标移动后要干两件事情,第一个是把圈的位置改动,第二个是确定图片的偏移。 圈的位置怎么动呢,应该是鼠标在哪里,圈的圆心就应该在哪里,同时鼠标移动我们能获得当前鼠标的位置。由于块的固定定位的目标在左上角,因此整个块的左偏移`left`属性应该是圆心位置减去半径长度,同理上偏移`top`也是一样。 图片怎么偏移呢,物理上讲运动是相对的。我们应该考虑,鼠标往左动相当于图片往右动,同理鼠标往下动就是图片往上动。由于要模拟看图片的不同位置,因此鼠标移动后的位置和图片的位置应该是相反数。这样,图片也能移动到正确的位置。 圈移动的位置和图片移动的位置配合好之后,就真的模拟出以孔窥图的效果了。这部分的代码如下。 ``` function toggle_hidden_background() { if ($(".hidden-bg").is(":visible")) { $(".hidden-bg").hide(); $(".hidden-bg").unbind('mousemove'); } else { $(".hidden-bg").bind('mousemove' ,function(e) { $(".circle").css({'left': (e.clientX - 100) + 'px'}); $(".circle").css({'top': (e.clientY - 100) + 'px'}); $(".circle").css({'background-position': -(e.clientX - 100) + 'px ' + -(e.clientY - 100) + 'px'}); }); $(".hidden-bg").show(); } } ``` 不过弟弟的网站实现和我不太一样,具体的逻辑没有摸透,它图片的偏移和圈的偏移还不完全是相反数,有一定偏差,不过我这种方案效果还可以,也就不深究了。 [1]: https://mygalgame.com

Read More

博客表情的实现方法

Satori

Author: Satori

Posted: 2017-12-10 11:22:13

Category: CSS Django HTML JavaScript 技术

Views: 1395 Comments: 0

## 一、背景 在博客和评论里面插入各种表情是非常常见的,虽然网上有各种各样的包,但是普遍都不能自主添加表情以及动态添加分组。因此我需要自己实现一个表情系统。 表情系统的实现还是比较复杂的,难点在于我想要实现一个组件一样的表情栏,在网页相应的地方只需要简单的引用这个组件就能实现在这个组件所在的表单里面的输入框插入表情。 ## 二、总体思路 ### 1. 总体流程 实现表情功能主要分为两步,第一步是实现一个表情组件,按这个组件的某一个表情能插入一段对应该表情的文字,第二步是前端将这个表情文字进行转化,转换成表情图片。 ### 2. 设计方法 对于表情组件而言,需要让它处在一个表单之中,而且这个表单有且仅有一个`textarea`元素,当表情组件某个表情被点击的时候,自动向这个`textarea`光标处插入对应该表情的文字。 对于前端转化而言,需要正则提取表情文字,并将它替换成对应的`img`标签图片。 ## 三、具体实现 ### 1. 表情组件的实现 使用纯后端的方法来实现。首先需要考虑表情的动态添加以及表情组的动态添加问题,因此需要把一类表情放到表情组文件夹里面,然后在每个文件夹添加对应的表情。后端只需要读取对应目录的文件就能动态地得到表情组以及每个表情。 而要实现处处能引用这个组件,我选择的是使用`template tag`来返回组件的html代码。这样每次只需要在其他template引用这个tag就可以得到表情组件了。 接着只需要拼接好html代码,将读到的文件及文件夹名字进行解析,就可以得到表情组名以及每个表情组下每个表情的名字。然后封装成`img`标签,同时为了方便之后插入表情文字,将表情所属的组名以及表情的名字装入`img`标签的数据段里面。 具体代码如下。 ``` @register.simple_tag def emoticon_bar(): cur_dir = os.path.join(settings.MEDIA_ROOT, "emoticon") emoticon_set_names = [x for x in os.listdir(cur_dir) if os.path.isdir(os.path.join(cur_dir, x))] # sort by the ascii code emoticon_set_names = sorted(emoticon_set_names) tab_list = "" img_list = "" for set_ind, emoticon_set_name in enumerate(emoticon_set_names): emoticon_dir = os.path.join(cur_dir, emoticon_set_name) files = [x for x in os.listdir(emoticon_dir) if os.path.isfile(os.path.join(emoticon_dir, x))] # sort the file by ascii code files = sorted(files) urls = [settings.MEDIA_URL + "emoticon/{}/{}".format(emoticon_set_name, x) for x in files] cur_img = "" for ind, url in enumerate(urls): cur_img += """<img src="{}" data-filename={}>""".format(url, files[ind]) cur_set = """ <div class="emoticon-set" data-emoticon_set_name="{emoticon_set_name}"> {img_str} </div> """.format(emoticon_set_name=emoticon_set_name, img_str=cur_img) img_list += cur_set tab_list += """ <a class="btn btn-default" data-ind={set_ind}>{set_name}</a> """.format(set_ind=set_ind, set_name=emoticon_set_name) string = """ <p>Emoticons:</p> <div class="emoticon-div"> <div class="emoticon-tab"> {tab_list} </div> <div class="emoticon-img"> {img_list} </div> </div> """.format(tab_list=tab_list, img_list=img_list) return mark_safe(string) ``` 这样在后面的模板里面,只要引用`{% emoticon_bar %}`就能得到表情组件了。 但只靠后端也不能实现组件的功能。因为前端需要控制按下表情后向`textarea`插入相应表情文字的工作,这需要用js实现,定义一个函数,传入一个textarea的jquery对象,再传入需要插入的文字。实现方法主要是先得到`textarea`光标的起始位置和终止位置,将这个位置的所有字符替换为text即可,同时还需要还原插入后光标的位置,只需要让新光标移动text的长度即可。代码如下。 ``` function make_text(textarea, text) { var s = textarea.get(0).selectionStart; var e = textarea.get(0).selectionEnd; var val = textarea.get(0).value; textarea.get(0).value = val.substring(0, s) + text + val.substring(e); textarea.get(0).selectionStart = e + text.length; textarea.get(0).selectionEnd = e + text.length; textarea.focus(); } ``` 接下来是定义每个表情被按下的操作,主要是找到是哪个图片被按下来确定插入的表情文字,以及找到需要插入的`textarea`的位置。找插入文字可以利用之前后端传过来的`emoticon-set`的`data-emoticon_set_name`属性来确定类别,再用该图片的`data-filename`属性得到名称。再进行拼接。我使用的表情文字定义是`[[set_name空格file_name]]`,原因是尽量避免与markdown的`[][]`的冲突。然后是找到`textarea`的位置,具体做法是找到这个表情组件所属的form,再在form的后代里面找`textarea`。最后调用`make_text`函数即可实现文字的添加。代码如下。 ``` $(".emoticon-set img").click(function(){ var set_name = $(this).parent().attr("data-emoticon_set_name"); var file_name = $(this).attr("data-filename"); var string = "[[" + set_name + " " + file_name + "]]"; // find the nearest ``textarea`` above the current bar var textarea = $(this).closest("form").find("textarea"); make_text(textarea, string); }); ``` 至于控制组件的其他点击,比如单击不同组,表情的切换控制不是非常核心的代码,简要思路就是使用一个data标签来确定哪个组被选定了,判断是第一次点击标签还是标签跳转还是点击同样标签,这三种情况对应处理一下即可。 关于css美化简要说要让用户快速得知自己正在点击的按钮,让鼠标悬停的时候将图片的不透明度改变,并调整大小,代码如下。 ``` .emoticon-set img { cursor: pointer; transition: transform 0.5s; opacity: 0.6; } .emoticon-set img:hover { transform: scale(1.2, 1.2) rotate(18deg); opacity: 1; } ``` ### 2. 表情转换的实现 这一部分相对就简单很多了,其实只需要将所有的字串正则替换为img标签,代码如下。 ``` html = html.replace(/\[\[([^ \[\]]+?) ([^ \[\]]+?)]]/g, '<img src="/media/emoticon/$1/$2">'); ``` 同时注意为了跟原有markdown协同工作,需要抽象出一个统一的render接口,首先渲染markdown,然后渲染表情,函数如下。 ``` function render_content(text) { // first default markdown render var renderer = new marked.Renderer(); renderer.code = function (code, language) { return '<pre class="prettyprint"><code class="hljs">' + hljs.highlightAuto(code).value + '</code></pre>'; }; var html = marked(text, { renderer: renderer }); // render the emoji html = html.replace(/\[\[([^ \[\]]+?) ([^ \[\]]+?)]]/g, '<img src="/media/emoticon/$1/$2">'); return html; } ``` ## 三、总结 表情这个功能总体实现难度还是不小的,要做到鲁棒性以及可重用性才是真正的难点。这种设计缺陷在于需要让组件置于一个form内,而且form内还需要只有一个textarea才能正常工作,因此还需要考虑一下更好的方法。

Read More

网页vim键位浏览博客及评论实现方法

Satori

Author: Satori

Posted: 2017-10-07 12:25:41

Category: CSS HTML JavaScript vim键位 技术

Views: 932 Comments: 4

## 一、背景 这个功能的想法最早来源于北邮人论坛。当时我刚会用vim,在浏览论坛的时候我不禁试了一下,结果发现真的可以用vim键位,表示很欣喜。这个功能在我刚开始开发这个博客就考虑了,但是当时还有很多其它更重要的功能要开发,结果基本上拖了半年才真正实现了这个功能,算是完成了一个心愿。网页快捷键浏览在其他网站也有,比如twitter可以用j和k来上下切换浏览。 ## 二、需求分析 因为后期开发了用户界面,因此用户界面的条目,包括发表的博客,发表的评论,回复我的等等都需要能使用vim键位浏览。同时,在博客详情页面,也能通过vim键位来浏览评论。 这里需要定义一下什么叫用vim键位浏览。就是说当使用vim键位的时候,会选中目标的博客,将它移动到屏幕中央,并在外边框加上颜色进行标注。在浏览博客条目的时候,按照vim键位,`j`就是向下移,`k`就是向上移,`h`就是翻上一页,`l`就是翻下一页。 在博客详情页面中,因为评论是树形结构,因此键位的意义也就稍有不同。`j`就是按照评论树的先序遍历顺序取下一个评论,同理`k`就是取上一个。`h`定义为评论树当前结点的前一个兄弟结点,同理`l`就是下一个兄弟节点。 在键位激活的时候,要做两件事情,一件是将目标窗口标红,另一件是将屏幕移动到目标窗口。这里也就分列表页面和详情页面来讲实现方法。不过在此之前先讲这两个功能都需要实现的逻辑,也就是移动窗口和给窗口加上边框。 ## 三、移动窗口及边框着色的实现方法 ### 1. 移动屏幕到目标窗口 这一部分主要是实现当用户按下按键之后移动到目标窗口的方法。这里将这个操作封装为一个函数。传入目标窗口以及移动时间。难点在于怎么计算目标窗口的位置。因为是要移动到屏幕中央,因此先判断能不能移到中央。这个取决于目标块的大小。如果目标窗口的高度都比屏幕高度高了,那就不能完整显示,就只能将它移动到屏幕顶端。如果不是的话,就能移动到中央。 首先需要明白怎么样使窗口移动。这里采用的做法是让**整个页面**移动,而不是让该窗口移动。可以理解为屏幕是绝对不动的,动的只有页面,页面在屏幕里面滑动,而屏幕只显示页面当前滑动的位置。使用`scrollTop`的动画来实现整个页面的移动。这个动画需要给出位移。在这种设计下,位移多少就是整个页面上滑动多少。 举个例子,如果位移为0的话页面不移动,位移为100的话页面向上移动100px,可以想象,如果计算出某元素顶部距离页面的距离,比如距离为x,位移x就能刚好移动到那个元素的顶部,也就是屏幕顶部和元素顶部重合。 因此剩下的事情就简单了,首先算出该窗口距离顶部的位移。如果直接让页面移动那么多的话那个窗口就在屏幕顶端了,因此需要页面移动少一点,移动到窗口居中就行了,下面就算少多少距离。这个就简单了,当前窗口需要移动到屏幕底端一半的位置,也就是屏幕高度减去窗口高度除以2。只要页面少往上移动这么多距离就能使窗口刚好居中。代码如下。 ``` function move_to_ele(ele, time) { // scroll to ele if (ele.size() === 1) { var elOffset = ele.offset().top; var elHeight = ele.height(); var windowHeight = $(window).height(); var offset; if (elHeight < windowHeight) { offset = elOffset - ((windowHeight / 2) - (elHeight / 2)); } else { offset = elOffset; } $('html, body').animate({ scrollTop: offset }, time); } } ``` ### 2. 给窗口边框着色 给边框着色就比较简单了,我这里也是封装成一个函数来处理。这个函数传两个参数,一个是当前已经被选中的窗口(没有的话就是空),以及下一个被选中的窗口(可能是下面一个也可能是上面一个窗口)。 着色方法比较简单,就是把当前窗口的`kb`类给移除,给下一个窗口加上`kb`类。而`kb`类定义了样式,也就是加边框。这样通过加上或者移除这个类来实现边框着色与否。`kb`样式如下。 ``` .kb { border: solid 4px rgba(255, 0, 0, 0.6); } ``` 函数实现如下。 ``` function color_to_ele(curEle, next) { if (next.size() === 1) { if (curEle !== null) { curEle.removeClass("kb"); } next.addClass("kb"); } } ``` ## 四、列表的定位 ### 1. 找到当前激活的窗口 首先需要找是否已经有窗口被激活了,如果没有的话按`j`从第一个开始浏览,如果有的话就从当前位置开始。我们根据div的class属性来判断是否有激活窗口,有的话就返回那个窗口。代码如下。 ``` function getCurrentElement() { var ele = $(".blog-div.kb"); if (ele.size() != 1) { return null; } else { return ele; } } ``` ### 2. 四个键位的定位实现 vim键位的`j` `k` `h` `l`分别对应下移,上移,左翻一页,右翻一页。对于上下移动,调用next方法来获取下一个激活窗口的位置,使用prev方法得到上一个激活窗口的位置。得到目标位置后,再对新窗口的边框进行着色,并移动到对应位置。对于翻页操作,只需模拟点击左翻和右翻按钮来实现。同时需要注意边界情况,比如没有上一页或者下一页,没有上一个窗口或者下一个窗口等等,代码如下。 ``` function movedown(time) { var curEle = getCurrentElement(); var next; // no item selected if (curEle === null) { next = $(".blog-div").first(); } else { next = curEle.next(".blog-div"); } // if not the last item color_to_ele(curEle, next); move_to_ele(next, time); } function moveup(time) { var curEle = getCurrentElement(); // no item selected if (curEle !== null) { var next = curEle.prev(".blog-div"); color_to_ele(curEle, next); move_to_ele(next, time); } } function moveprev() { var btn = $("#prev-btn"); if (btn.size() === 1) { btn[0].click(); } } function movenext() { var btn = $("#next-btn"); if (btn.size() === 1) { btn[0].click(); } } ``` ### 3. 绑定键盘按键 最后一步就是将四个按键和刚才的四个函数相连接,设置一个移动时间即可。最后需要注意用户在输入框打字的时候不要监听这四个快捷键。代码如下。 ``` var time = 100; var vim_key_listener = new window.keypress.Listener; vim_key_listener.simple_combo("j", function() { movedown(time); }); vim_key_listener.simple_combo("k", function() { moveup(time); }); vim_key_listener.simple_combo("h", function() { moveprev(); }); vim_key_listener.simple_combo("l", function() { movenext(); }); $("input[type=text], textarea") .bind("focus", function() { vim_key_listener.stop_listening(); }) .bind("blur", function() { vim_key_listener.listen(); }); ``` ### 4. 用户界面的特殊处理 因为用户界面有多个tab,每个tab的窗口类名都一样,因此需要甄别是哪个窗口被激活,保证调用`getCurrentElement`函数的时候只会得到一个元素,就是当前tab的窗口。因此需要在用户界面点击tab的时候,将其他tab中的内容全部清空,再利用ajax请求得到自己tab的内容,这样就解决这个问题了,代码如下。 ``` function clear_tabs() { $("#blog-posts").empty(); $('#comment-posts').empty(); $('#comment-received').empty(); $("#blog-subscriptions-div").empty(); } $('a[data-toggle="pill"]').on('shown.bs.tab', function (e) { var target = $(e.target).attr("href");// activated tab clear_tabs(); }); ``` ## 五、评论的定位 ### 1. 找到当前激活的评论 大体和前面一样的做法,只是将类名改为`comment-body`,代码如下。 ``` function getCurrentElement() { var ele = $(".kb .comment-body").first(); if (ele.size() != = 1) { return null; } else { return ele; } } ``` ### 2. 四个键位的定位实现 vim键位的`j` `k`分别对应dom树的先序遍历的下、上一个结点。`h` `l`对应同级兄弟结点的上、下一个结点。 对于先序遍历的结果的获取,使用jQuery选取所有类为`comment-body`的元素,这样获取的列表默认就是dom树先序遍历的结果,然后只需找到当前激活窗口在这个列表的位置,选取上一个或者下一个位置就能得到先序遍历的前后结点。 对于上下兄弟结点的获取,直接找到它的父节点并在父节点调用next方法获取下一个list的位置,同理prev方法得到上一个list位置,也就是找到了兄弟结点的父亲结点。然后只需找那个父亲结点的孩子就能找到下一个窗口的位置。 得到目标位置后,再对新窗口的边框进行着色,并移动到对应位置。同时需要注意边界情况,比如没有上一个或下一个先序遍历的位置,没有上一个或下一个兄弟等等,代码如下。 ``` function movenext(time) { var curEle = getCurrentElement(); var next; // no item selected if (curEle == = null) { next = $(".comment-body").first(); color_to_ele(null, next.parent()); } else { next = curEle.parent().next("li").children(".comment-body"); color_to_ele(curEle.parent(), next.parent()); } // if not the last item move_to_ele(next, time); } function moveprev(time) { var curEle = getCurrentElement(); var next; // no item selected if (curEle != = null) { next = curEle.parent().prev("li").children(".comment-body"); color_to_ele(curEle.parent(), next.parent()); // if not the last item move_to_ele(next, time); } } function movedown(time) { var curEle = getCurrentElement(); // no item selected if (curEle == = null) { var next = $(".comment-body").first(); color_to_ele(null, next.parent()); move_to_ele(next, time); } else { var all_items = $(".comment-body"); var cur_ind = all_items.index($(".kb .comment-body").first()); if (cur_ind + 1 <= all_items.size() - 1) { var next = $(all_items.get(cur_ind + 1)); color_to_ele(curEle.parent(), next.parent()); move_to_ele(next, time); } } } function moveup(time) { var curEle = getCurrentElement(); // no item selected if (curEle != = null) { var all_items = $(".comment-body"); var cur_ind = all_items.index($(".kb .comment-body").first()); if (cur_ind - 1 >= 0) { var next = $(all_items.get(cur_ind - 1)); color_to_ele(curEle.parent(), next.parent()); move_to_ele(next, time); } } } ``` ### 3. 绑定键盘按键 最后一步就是将四个按键和刚才的四个函数相连接,设置一个移动时间即可。最后需要注意用户在输入框打字的时候不要监听这四个快捷键。代码如下。 ``` $(document).ready(function() { var time = 100; var vim_key_listener = new window.keypress.Listener; vim_key_listener.simple_combo("j", function() { movedown(time); }); vim_key_listener.simple_combo("k", function() { moveup(time); }); vim_key_listener.simple_combo("h", function() { moveprev(time); }); vim_key_listener.simple_combo("l", function() { movenext(time); }); $("input[type=text], textarea") .bind("focus", function() { vim_key_listener.stop_listening(); }) .bind("blur", function() { vim_key_listener.listen();}); }); ``` 至此用vim键位浏览博客列表,用户列表,博客评论的实现方法也就全部实现了。

Read More

网页模拟钢琴键盘的实现方法

Satori

Author: Satori

Posted: 2017-10-06 23:05:01

Category: HTML JavaScript 技术 钢琴键盘

Views: 1531 Comments: 2

## 一、简介 这是键盘快捷键的第二个应用。这个想法最早来自一款叫做Mountain的steam游戏。这个游戏就是每个字母按键都对应一个音符,通过弹奏某些著名乐章的旋律来解锁一些成就。不够Mountain最大的问题是它只能弹C大调(或者A小调),也就是说,它只有C D E F G A B这7种音,根据十二平均律还少了5个半音,分别是C#(Db),D#(Eb),F#(Gb),G#(Ab),A#(Bb),因此我想实现一个能弹奏这十二种半音的钢琴键盘。 除了能弹奏十二种半音外,我还希望音域能宽一点,至少钢琴的88个半音都能涉及到。但是键盘按键又没有那么多,所以还需要两个按键来让键盘整体平移八度。如果能准备不同音色的乐器那就更好了。 ## 二、实现方法 ### 1.音的演奏。 一种做法是将所有的音作为静态文件进行播放,但是这样会有网络延迟带来的一系列问题,不是最佳的解决方法。幸好有JavaScript库可以使用。这里用的是[audiosynth][1],它通过调用简单的函数就能播放声音。 使用`Synth.setVolume`来调节播放音量,通过以下调用播放声音。 ``` Synth.play(instrument, note, oct + octave_base, note_span); ``` play函数接受4个参数,第一个是整形,表示乐器种类,这个库提供4种音色,0代表钢琴,1代表管风琴,2代表吉他,3的声音很迷,不知道是什么。第二个参数是演奏哪个音,是一个字符串,表示形如`C`,`C#`之类,降号需要转换成升号,比如`Db`就要转换为`C#`。第三个参数表示音是哪个八度的,整形,比如4就代表第4个八度。最后一个参数表示演奏的时间,单位是秒。 ### 2.键盘的对应 键盘的一个按键对应一个音。为了符合钢琴的键位,定义`q` `w` `e` `r` `t`这一行是低音`C` `D` `E` `F` `G`等等。`q`和`w`上面的按键为`2`,代表`C#`,`w`和`e`上面是`3`,代表`D#`等等。最后的`]`代表`G`,接着,键盘从`z` `x` `c` `v` `b`这一行开始,分别代表`A` `B` `C` `D` `E`这几个音,刚好和上一排对应上,最后一个音为`.`,也就是`B`。这样下来,这个键盘包含3个完整的8度。使用字典来记录这个规则。代码如下。 ``` var keymap = { 'q': 'C -1', 'w': 'D -1', 'e': 'E -1', 'r': 'F -1', 't': 'G -1', 'y': 'A -1', 'u': 'B -1', 'i': 'C 0', 'o': 'D 0', 'p': 'E 0', '[': 'F 0', ']': 'G 0', 'z': 'A 0', 'x': 'B 0', 'c': 'C 1', 'v': 'D 1', 'b': 'E 1', 'n': 'F 1', 'm': 'G 1', ',': 'A 1', '.': 'B 1', '2': 'C# -1', '3': 'D# -1', '5': 'F# -1', '6': 'G# -1', '7': 'A# -1', '9': 'C# 0', '0': 'D# 0', '=': 'F# 0', 'a': 'G# 0', 's': 'A# 0', 'f': 'C# 1', 'g': 'D# 1', 'j': 'F# 1', 'k': 'G# 1', 'l': 'A# 1' }; ``` 其中每一行的键为按键,值为字符串,前半部分表示音的种类,后半部分表示相对音高,相当于基准音的偏移。-1代表比基准低一个八度,1代表高一个八度。如果定义基准音高为4的话,`q`代表`C3`,`i`代表`C4`,`c`代表`C5`。 ### 3.重复按键的处理 重复按键问题主要出现在一直按着一个键不放的情况。比如一直按着`c`,就会一直不断演奏这个音,这显然不是我们想要的,我们想至少需要等到按键释放后再按才再次触发这个音的播放。于是按键处理这一块就需要用到高级函数了,需要我们自己定义按键按下和释放的处理函数。 最简单的思路就是记录每一个按键的状态,记录是否已经被按下,如果被按下了,就不再播放这个音。于是整个过程是这样。首先所有按键的状态都是没有被按下。当有一个按键被按下的时候,先判断这个按键是否已经被按下了,如果已经被按下了,说明之前那次按键还没有释放,直接跳过,如果没有被按下,就播放声音,并将那个按键的标志设为按下。当某一按键释放的时候,将那个键的状态设为没有被按下。整个过程代码如下。 ``` for (var key in keymap) { key_state[key] = false; key_func_map.push({ "keys": key, "on_keydown": function(event) { var key = event.key.toLowerCase(); if (key_state[key] === false) { var cur_str = keymap[key]; var note = cur_str.split(" ")[0]; var oct = parseInt(cur_str.split(" ")[1]); Synth.play(instrument, note, oct + octave_base, note_span); key_state[key] = true; } }, "on_keyup": function(event) { var key = event.key.toLowerCase(); key_state[key] = false; } }); } ``` 其中`key_state`记录按键状态,`key_func_map`存储按键映射,`octave_base`表示当前的音高。 ### 4.音高以及乐器的调整 因为想让键盘能弹出多个八度,因此需要将整体音域平移,需要两个按键,一个上移,一个下移。用方向键上下即可。同时还希望能改乐器,同理使用左右来切换乐器。当然需要处理边界条件,比如音的上界和下界,乐器的编号的上界和下界。同样使用`key_func_map`来记录按键映射,代码如下。 ``` key_func_map.push({ "keys": 'up', "on_keydown": function() { if (octave_base <= 6) { octave_base++; } } }); key_func_map.push({ "keys": 'down', "on_keydown": function() { if (octave_base >= 2) { octave_base--; } } }); key_func_map.push({ "keys": 'left', "on_keydown": function() { if (instrument >= 1) { instrument--; } } }); key_func_map.push({ "keys": 'right', "on_keydown": function() { if (instrument <= 2) { instrument++; } } }); ``` 其中`octave_base`记录当前基准音高,`instrument`记录当前乐器的编号。 ### 5.钢琴键盘的开启和关闭 首先使用一个listener将刚才`key_func_map`记录的按键映射进行监听,并进行初始化,比如先停止监听,设定初始播放音量等,代码如下。 ``` var instrument_listener = null; instrument_listener = new window.keypress.Listener; Synth.setVolume(0.4); instrument_listener.register_many(key_func_map); instrument_listener.stop_listening(); ``` 然后再用另一个listener来启动或者关闭这个钢琴键盘。同样使用按键序列来实现。通过一个变量`instrument_active`来记录钢琴键盘是否已经被激活,再根据激活状态来判断下面是激活还是无效钢琴键盘,代码如下。 ``` function toggle_keyboard() { if (instrument_active === false) { instrument_active = true; instrument_listener.listen(); } else if (instrument_active === true) { instrument_active = false; instrument_listener.stop_listening(); } } key_listener.sequence_combo("! @ #", toggle_keyboard, true); ``` 这样一来钢琴键盘的实现也就全部完成了。 [1]: https://github.com/keithwhor/audiosynth

Read More

网页背景切换的实现方法

Satori

Author: Satori

Posted: 2017-10-06 16:53:10

Category: CSS HTML JavaScript 技术

Views: 1089 Comments: 0

## 一、简介 在实现了网页键盘的处理后,下一步就是利用键盘快捷键实现一些功能了。如果还没有看过键盘快捷键的实现,请移步[这篇文章][1]。 这篇主要讲键盘快捷键实现网页背景切换的实现方法,最后顺便题一下只显示背景的方法。 ## 二、背景切换的实现方法 因为背景切换实现非常简单,因此放到第一个讲。所谓的背景切换就是指在静态背景(健全壁纸)和动态背景(可能不是很健全的壁纸)的切换。如果对动态壁纸实现方式感兴趣,请移步[这篇文章][2]。 实现的关键在于key的回调函数实现。这个函数要做的事情是判断现在是什么背景,然后接不接受这次切换请求。因为在小屏幕的时候是不显示动态背景的。具体的细节就是使用window的`max-width`属性来判断是否是小屏幕,然后用hidden-xs是否可见来判断是否是动态背景。因此很容易写出以下逻辑。 ``` function toggle_background() { if (!window.matchMedia('(max-width: 768px)').matches) { if ($(".hidden-xs").is(":visible")) { hide_background(); Cookies.set("animated_background", false, { expired: 14 }); } else { show_background(); Cookies.set("animated_background", true, { expired: 14 }); } } } ``` 值得注意的是切换背景的时候设置一个Cookie,来记录用户偏爱的背景模式,不用每次来都切换背景。每次加载页面的时候先读取这个cookie,代码如下。 ``` var is_animated_background = Cookies.get("animated_background"); if (is_animated_background === "true") { show_background(); } else { hide_background(); } ``` 使用`hide_background`来隐藏动态背景,使用`show_background`来显示动态背景。于是下面给出这两个函数的实现。 ``` function hide_background() { $(".hidden-xs").hide(); $(".simple-image-div").css('display', ''); } function show_background() { $(".hidden-xs").show(); $(".simple-image-div").css('display', 'none'); } ``` 其中静态壁纸装到`simple-image-div`当中,动态壁纸装到`hidden-xs`中。静态壁纸的构造和动态壁纸一样,也是采用100%高度和100%宽度覆盖屏幕的方式。但是有一个问题,就是静态背景在刷新的时候会闪一下,非常瞎眼。 瞎眼的原理这里稍微多说一下。在刷新的时候,这时你的屏幕是背景图片。刷新是需要过程的,可能是0.5秒的时间,这段时间,最先加载的是背景的颜色,也就是`background-color`,然后再是图片。因此整个过程你看到的是背景图片,纯色快,背景图片。如果你的背景图片是暗色的,背景颜色设置的是浅色,就会出现深色-浅色-深色的转换,而且中间过程很短,所以才会感觉眼睛被晃了一下。 解决这个问题我的确花了不少时间。最后比较好的解决方法是先将背景设置为黑色,然后给静态图片设置从黑到背景图片的过渡,这样就不会瞎眼了。整个过程采用css解决。 ``` @keyframes simpleImage { 0% { opacity: 0; } 8% { opacity: 0; } 17% { opacity: 1; } 25% { opacity: 1; } 100% { opacity: 1 } } .simple-image-div { position: fixed; width: 100%; height: 100%; top: 0px; left: 0px; color: transparent; background-image: url('/static/mysite/images/background-lg.png'); background-size: cover; background-position: 50% 50%; background-repeat: none; opacity: 0; z-index: -2; animation: simpleImage 6s linear 0s; animation-fill-mode: forwards; } ``` 以上代码看不懂可以先看[动态背景实现方法][2],里面有讲`keyframe`之类的知识。 最后绑定一个按键listener即可,代码如下。 ``` var key_listener = new window.keypress.Listener; key_listener.sequence_combo("left up right down", toggle_background, true); ``` ## 三、只显示背景的实现方法 讲完背景的切换还是顺便讲一下只显示背景的方法。这个也就很简单了,将主页面的div隐藏,再隐藏标题栏就好了,随后将这个函数跟按键listener绑定即可实现。代码如下。 ``` function toggle_content() { $("#main-content").toggle(); $("#navigation-bar").toggle(); } key_listener.sequence_combo("up up down down left right left right b a", toggle_content, true); ``` [1]: https://chongliu.me/blogs/archive/136/ [2]: https://chongliu.me/blogs/archive/135/

Read More

网页键盘快捷键的实现方法

Satori

Author: Satori

Posted: 2017-10-06 12:15:33

Category: HTML JavaScript 技术

Views: 1684 Comments: 0

## 一、前言 对于网页而言,加入快捷键可以提供一些额外功能,这些功能可能在某种原因下不太适合在网站明处提供入口(说的就是背景233)因此实现一个快捷键是比较有意思的事情。 同时我们希望快捷键就像开关一样,通过同样的键位可以随时打开随时关闭。 ## 二、实现概述 捕捉键盘的按键其实并不困难,在原生JavaScript就有`keydown`和`keypress`事件,只需要绑定这个事件,通过`event.keyCode`就能获得按下的到底是什么键。但是这样最大的问题就是不太方便处理连续按键的问题。比如按键组合`up up down down left right left right b a`,一个一个识别是不太好维护的,而且换一套组合又需要重写。所以通用的做法是根据按键自动构造一个自动机。而且还需要对监听可以随时移除随时添加。因此需要写一个类来专门实现这些功能。 因此,我选择用已有的库,自己写这个实在是太累了。我选择的是[keyPress][1]库,这个库的好处是无论是组合键还是按键组合都能处理,比较强大。 ## 三、keyPress库简介 首先需要在html文件里面引用这个js文件。然后进行初始化,代码如下。 ``` var listener = new window.keypress.Listener(); ``` 这样就得到一个listener对象,再对listener对象绑定一个按键规则即可实现监听。listener支持按键组合和组合键,下面都简要介绍一下。 对于组合键的支持,是通过`simple_combo`函数实现的,它的参数是按键组合的字串和回调函数。实例如下。 ``` listener.simple_combo("shift s", function() { console.log("You pressed shift and s"); }); ``` `shift s`代表的是按键组合是shift加上s,后面回调的函数用于显示信息。 对于按键组合的支持,是通过`sequence_combo`函数实现的,它的参数是按键序列和回调函数,示例如下。 ``` listener.sequence_combo("up up down down left right left right b a enter", function() { lives = 30; }); ``` 这是keypress最简单的用法,如果要定义比较复杂的逻辑,要精确到按下和释放操作,或者要增加例如互斥,只按一次等等的高级属性, 就需要用它的api,也就是`register_combo`函数了。它的参数及说明如下。 ``` listener.register_combo({ "keys" : null, "on_keydown" : null, "on_keyup" : null, "on_release" : null, "this" : undefined, "prevent_default" : false, "prevent_repeat" : false, "is_unordered" : false, "is_counting" : false, "is_exclusive" : false, "is_solitary" : false, "is_sequence" : false }); ``` `keys`就是按键的序列串,`on_keydown`就是按下的后执行的函数,`on_keyup`就是释放后执行的函数,`on_release`就是当所有按键按完后执行的函数。`this`是执行回调函数的域对象。`prevent_default`是指定对每一个按键是否要执行preventDefault,`prevent_repeat`是指定这个按键操作是否只监听一次,`is_unordered`指定监听的按键序列是否要按顺序进行敲击,`is_counting`指定是否记录组合键按键的次数,`is_exclusive`指定按键是否互斥,互斥组合之间序列较短的组合会被激活,长的会被抛弃,`is_solitary`指定当有额外的按键被按下的时候组合键是否被激活,`is_sequence`指定这个按键是否是序列按键。 同时keypress还提供了`register_many`方法,通过传入按键设定字典来注册一系列按键,以及`unregister_combo`来注销监听,以及`stop_listening`来暂停监听,`listen`来启用监听等方法。这些会在后面的网站功能实现模块来介绍具体用法。 [1]: https://github.com/dmauro/Keypress

Read More

网页动态背景实现方法

Satori

Author: Satori

Posted: 2017-09-24 19:56:01

Category: CSS HTML 动态背景 技术

Views: 4143 Comments: 12

## 一、简介 网页动态背景最早是在忧郁的弟弟的galgame网站上看到的,感觉效果很棒自己也想试一试。动态背景效果主要是动态地切换几张背景图片。图片之间的切换应该比较平滑和流畅。在研究他网页前端代码的时候,我总算了解它的实现方法。 ## 二、实现方法 首先背景要实现多张图片的切换,直接改body的background-image不是一个好的选择,因为这个属性只能绑定一张图片。在图片切换的时候只能是一张图片从有过滤到无,在换另一张图片从无到有,这样做会导致图片交替的时候中间出现一段时间的空层,无法做到两张图片同时存在通过不透明度来控制的切换,显得比较生硬。因此不能简单地通过改background-image来切换。 因此使用另外的方法,将图片放到一个div里的list里面,再写css控制切换显示。 因此代码结构是这样的。 ``` <section class="hidden-xs"> <ul class="cb-slideshow"> <li><span style="background-image: url('')"></span></li> <li><span style="background-image: url('')"></span></li> <li><span style="background-image: url('')"></span></li> <li><span style="background-image: url('')"></span></li> <li><span style="background-image: url('')"></span></li> <li><span style="background-image: url('')"></span></li> </ul> </section> ``` 只需将每个list的span里的background-image的属性填上就行了。 但是有一个非常重要的问题,就是每次我想给用户随机的图片,意思是每次的url地址都不一样。因此这个地址一定是服务器后端传给前端的。于是这里又有两个解决方案,一种是前端页面加载出来之后发一个ajax请求得到地址,这个解决方案很好,但是不知怎么的最近不太想写ajax请求,最要紧的是我希望页面刷出来的时候图片已经加载好了,所以ajax请求可能会出一点点问题。 第二种方案是在django的template层面上定义几个字段,每次在view层将地址传给template。但是这样做最大的问题是我所有view都需要写加载图片这个逻辑,显然这是非常糟糕的方法。 于是出现了第三种方法,就是定义template tag,将公共逻辑抽取到base.html,这个tag实现往base.html写入前端代码,因此就只用写一次逻辑就行了。 template tag逻辑如下。 ``` from django import template from django.utils.safestring import mark_safe from django.conf import settings import os import random register = template.Library() @register.simple_tag def load_background(): image_show_count = 6 # cannot be larger than the amount of images curdir = os.path.join(settings.MEDIA_ROOT, "background") items = os.listdir(curdir) selected = [items[i] for i in random.sample(range(len(items)), image_show_count)] urls = ["/media/background/{}".format(x) for x in selected] return mark_safe(""" <section class="hidden-xs"> <ul class="cb-slideshow"> <li><span style="background-image: url('{}')"></span></li> <li><span style="background-image: url('{}')"></span></li> <li><span style="background-image: url('{}')"></span></li> <li><span style="background-image: url('{}')"></span></li> <li><span style="background-image: url('{}')"></span></li> <li><span style="background-image: url('{}')"></span></li> </ul> </section> """.format(*urls)) ``` 这样就随机加载图片url,并返回一段html代码。 接着就需要在base.html里这么加载这个template tag。代码如下。 ``` {% load mysite_tags %} <body> {% load_background %} ...... </body> ``` 这样就在body之后加载了这段html,就能实现动态图片的加载了。 加载完图片后,剩下的问题就是如何实现动态背景的特效了。 由于不想使用JavaScript进行操作,因此写css样式进行加载。首先需要知道所有的动态都可以通过**`animation`**属性来定义。animation属性需要一个keyframe参数来指定这个元素怎么动,后面接持续时间,是否循环等等属性。而keyframe的定义是通过`@keyframe`字段,然后定义在整个动画过程的某几个关键点元素的属性是什么样的来实现。 首先是定义每个图片的animate属性,代码如下。 ``` .cb-slideshow li span { width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; color: transparent; background-size: cover; background-position: 50% 50%; background-repeat: none; opacity: 0; z-index: -2; -webkit-backface-visibility: hidden; -webkit-animation: imageAnimation 36s linear infinite 0s; -moz-animation: imageAnimation 36s linear infinite 0s; -o-animation: imageAnimation 36s linear infinite 0s; -ms-animation: imageAnimation 36s linear infinite 0s; animation: imageAnimation 36s linear infinite 0s; } ``` 最重要的属性是这几个。首先是width和height,这个都需要是100%,主要原因是需要所有的图片都充满全屏幕,然后是位置的三个属性,position top left,这几个定义了背景div是左上角开始,直接填充整个屏幕。background-position决定图片的位置在正中,background-repeat设为none避免图片被拼接。z-index使图片位于页面最底端避免遮住其他元素,最后animation定义了动画的keyframe是imageAnimation, 持续时间为36s,动画时间是线性流逝,重复次数为无限,初始相位为0。 下面是定义imageAnimation这个keyframe的动作,它控制图片的动画方式,代码如下。 ``` @keyframes imageAnimation { 0% { opacity: 0; animation-timing-function: ease-in; } 8% { opacity: 1; transform: scale(1.05); animation-timing-function: ease-out; } 17% { opacity: 1; transform: scale(1.1) rotate(0deg); } 25% { opacity: 0; transform: scale(1.1) rotate(0deg); } 100% { opacity: 0 } } ``` 这个定义如下,首先在0%,也就是初始阶段,图片的透明度为0,也就是全透明,时间进入方式为缓缓进入。在8%的时候,透明度为1,也就是全显示,transfrom定义scale为1.05,相当于放大1.05倍,时间函数为缓缓退出。同理25%和100%。总体来说一张图片的动画是先快速从透明度为0转到1,然后缓缓放大,接着缓缓地消失。 所以每张图片的动画效果都是这个。到了这里你可能会问了,既然每张图片都是这样,那么这些图片不就在同一时间做同样的动作,这样第一张图会盖住其他所有的图片,如何实现动态切换? 下面就是重点了,要让图片切换,就需要让它们在不同的时间点进行播放,也就是说,它们需要设置不同的延时,来错开彼此的播放时间。延时设置的代码如下。 ``` .cb-slideshow li:nth-child(1) span { } .cb-slideshow li:nth-child(2) span { animation-delay: 6s; } .cb-slideshow li:nth-child(3) span { animation-delay: 12s; } .cb-slideshow li:nth-child(4) span { animation-delay: 18s; } .cb-slideshow li:nth-child(5) span { animation-delay: 24s; } .cb-slideshow li:nth-child(6) span { animation-delay: 30s; } ``` 我们有6张图片要轮换显示,每个图片周期持续时间为36秒,结合上面的keyframe和animation的定义,也就是说,每张图片真正完整的显示时间为(17%-8%) * 36 = 3.24s,再加上0%到8%的过渡和17%到25%的过渡,我们在过渡中能看到的时间大概为9%,所以我们能看到图片的时间大概是6.48s,也就是说,设置时间间隔为6秒就能错开图片的显示。同时能有大概0.5s的时间来看到过渡(两张图片都是不完全显示,也就是说它们的透明度都在0和1之间)。所以上面的代码我们看到每个图片延时的间隔时间为6s,因此就能无限地动态显示图片。 至此动态图片的实现原理就全部讲完了,剩下需要注意的一点是装所有图片的div的class为`hidden-xs`,也就是说在小屏幕的时候这个div会消失,这时候背景就没有定义,默认情况下就是纯色,这不是我们想要的。设置`hidden-xs`的主要原因是小屏幕的时候背景图片根本就显示不全,没有任何意义,因此小屏幕就不显示动态背景了,于是需要在css文件里面定义小屏幕的背景。代码如下。 ``` @media (max-width: 768px) { body { background-color: #f0f0f0; background: url('/static/mysite/images/background.png'); background-attachment: fixed; } } ``` 至此背景这个功能才算真正完成了。

Read More

HTML5扫雷游戏的实现

Satori

Author: Satori

Posted: 2017-09-24 16:12:46

Category: CSS HTML JavaScript 扫雷 技术

Views: 1763 Comments: 2

## 一、概述 扫雷是一个很经典游戏,从windows3.0就有了,这个游戏的魅力在于推理和运气。网页版的扫雷使用html5+css+js来开发,最大的好处是只要有一个现代的浏览器(对,我就是在黑IE6),就能愉快的玩耍。 先说需求分析,首先是能自由调节扫雷的棋盘大小,雷的个数能自由设置。第一次点的位置及其周围8个方格不能有雷。再设置作弊功能,比如时间暂停和显示所有雷的功能。 再谈整体的设计思路。首先是算法的流程。第一步是根据玩家设置的大小参数生成棋盘,然后玩家首次点击后生成雷。然后显示点击之后的棋盘。接着就是重复接收玩家的点击,包括左键和右键的点击。每次左键点击完成后,判断玩家是否碰到雷,碰到雷游戏结束,显示玩家标错的旗,剩余的雷位置以及所有的空方块以及数字方块。如果没死,就判断玩家是否获胜,获胜则显示通关画面。 ## 二、详细设计 ### 1. 图像的来源 显示的方块用什么显示比较好呢?最简单的想法是每个方块贴一张png图片。但是这样做的问题主要是当棋盘很大的时候,用户需要放大,或者缩小,png图片大小是定死的,如果随意放大缩小很可能失真。而什么样的图形能自由适应各种大小呢?没错,就是**`svg`**图像。 svg是矢量图形,它是算出来的,每次大小有更新都重新计算各个元素的位置。网络上有很多关于svg图像的写法。svg本质是一个文本文件,不过浏览器可以根据这些文本将图像加载出来。比如下图就是我所有的图像。无论你怎么放大缩小,图像都不会失真。 ![mine.svg][1] ![0.svg][2] ![1.svg][3] ![2.svg][4] ![3.svg][5] ![4.svg][6] ![5.svg][7] ![6.svg][8] ![7.svg][9] ![8.svg][10] ![clock.svg][11] ![dark.svg][12] ![flag.svg][13] ![mines.svg][14] ![smile.svg][15] ![un.svg][16] ![wrong flag.svg][17] ### 2. 初始化 利用几个二维数组来记录游戏信息。包括记录是否有雷的map,用户是否已经访问的数组disc,用户是否设置旗帜的数组flg,以及记录每一个方格周围8个方格的雷总数的数组num。另外还需要一个是否设置了雷的标志,以及记录用户鼠标按下的方格的位置。其中需要给每个方格绑定一个点击监听的函数来记录点击的位置,这个主要是避免用户鼠标释放的时候不在同一个方格的情况,这种情况下用户一般是发现自己不小心点到雷上了才会移开鼠标,避免一按就死。 ### 3. 用户输入的处理 因为棋盘大小以及雷的个数用户都能自己输入,因此需要对用户的输入进行处理。首先如果输入不是数字则自动设置一个最小值4,然后检查雷的个数,其中因为为了使雷都能装进格子里,而且扫雷的规则是点击的块以及周围8个格子不能有雷,因此雷最大数值是长乘宽再减去9。再根据这个数去生成棋盘。 ### 4. 雷的设置 雷的设置时间是用户在第一次点击后触发的。首先需要记录用户的点击位置。然后将棋盘位置作为一个二元变量加入一个数组,当然用户点击的位置以及它周围8个格子的位置不加入数组,这样就得到了雷能够生成的位置集合。然后随机选一个数组的下标,作为雷的方格,然后将这个元素从原数组中pop出去。 ### 5. 图像的更新 图像的更新主要是用disc数组,flg,num数组来绘制。用户所有标记过旗的位置将图片的src更新为旗帜的url,同理将用户已经访问的格子按照num数组进行更新图片,表示已经发现的格子中每一个格子周围雷的情况。 ### 6. 鼠标左键的处理 首先是鼠标点下的时候,记录用户点下的位置。然后是释放的时候判断是否和点击在同一个格子,是的情况才进行处理。 处理的过程首先是需要判断是在哪种格子点下的。对于已经访问过的格子,如果周围8个格子都是已经访问过的或者被置旗的,点击无意义,点击已经置旗的格子,也是无效的,点击周围有位置格子的方格,则触发chord操作,如果点击的是未探索的方格,那么就触发dig操作。 对于dig操作,需要判断这个格子是否有雷,如果有,直接结束游戏,如果没有,看这个方块是什么方格,如果是数组方块,就将数字方块翻开,如果这个方块是空格子,那对周围8个格子都进行dig操作。 以上两个操作完成之后,需要判断是否赢了,标准就是所有不是雷的方格都被探索了,相当于disc数组都被探索完毕了。 对于chord操作,首先需要判断点击的这个数字方块周围未知方格数和数字是否相等,在相等的情况下才进行操作,这个操作将这个数字方格周围的未知方格都进行一次dig操作。 ### 7. 鼠标右键的处理 同理先检测按下和释放的位置是否为同一个方格,如果是,再进行进一步判断。如果这个方格是未知方格,才进行置旗操作。 ### 8. 胜利及失败画面 这个就很简单了,失败了需要标注哪些地方用户置旗正确,哪些地方置旗错误,哪些地方有雷,其余的数字以及空白格在哪里。至于成功画面就比较简单了,只需要将所有雷的地方换成笑脸图就好了233 [1]: https://chongliu.me/static/minesweeper/mine.svg [2]: https://chongliu.me/static/minesweeper/0.svg [3]: https://chongliu.me/static/minesweeper/1.svg [4]: https://chongliu.me/static/minesweeper/2.svg [5]: https://chongliu.me/static/minesweeper/3.svg [6]: https://chongliu.me/static/minesweeper/4.svg [7]: https://chongliu.me/static/minesweeper/5.svg [8]: https://chongliu.me/static/minesweeper/6.svg [9]: https://chongliu.me/static/minesweeper/7.svg [10]: https://chongliu.me/static/minesweeper/8.svg [11]: https://chongliu.me/static/minesweeper/clock.svg [12]: https://chongliu.me/static/minesweeper/dark.svg [13]: https://chongliu.me/static/minesweeper/flag.svg [14]: https://chongliu.me/static/minesweeper/mines.svg [15]: https://chongliu.me/static/minesweeper/smile.svg [16]: https://chongliu.me/static/minesweeper/un.svg [17]: https://chongliu.me/static/minesweeper/wrong%20flag.svg

Read More