elasticsearch

elasticsearch背景 https://www.cnblogs.com/Neeo/p/10304892.html

我们建立一个网站或应用程序,并要添加搜索功能,但是想要完成搜索工作的创建是非常困难的。我们希望搜索解决方案要运行速度快,我们希望能有一个零配置和一个完全免费的搜索模式,我们希望能够简单地使用JSON通过HTTP来索引数据,我们希望我们的搜索服务器始终可用,我们希望能够从一台开始并扩展到数百台,我们要实时搜索,我们要简单的多租户,我们希望建立一个云的解决方案。因此我们利用Elasticsearch来解决所有这些问题及可能出现的更多其它问题。

什么是elasticsearch?

 一个搜素服务器,它提供了一个分布式多用户能力的全文搜索引擎,也可以说是一个数据库.基于RESTful web接口,也可以作为数据库使用。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

elasticsearch特点

  • 分布式实时文档存储,并将每一个字段都编入索引,使其可以被搜索。
  • 实时分析的分布式搜索引擎。
  • 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。

应用场景

 可以作为一个搜素的插件应用到项目中

elasticsearch组织架构

elasticsearch中文档,类型,索引的关系

①elasticsearch是面向文档的,那么就意味着索引和搜索数据的最小单位是文档;

  • 自我包含,一篇文档同时包含字段和对应的值,也就是同时包含key:value
  • 可以是层次型的,一个文档中包含自文档,复杂的逻辑实体就是这么来的
  • 灵活的结构,文档不依赖预先定义的模式,我们知道关系型数据库中,要提前定义字段才能使用,在elasticsearch中,对于字段是非常灵活的,有时候,我们可以忽略该字段,或者动态的添加一个新的字段。
  • 文档是无模式的,也就是说,字段对应值的类型可以是不限类型的。

②类型是文档的容器,就像mysql一样,表是行的容器;

类型中对于字段的定义称为映射,比如name映射为字符串类型。

③索引是映射类型的容器,elasticsearch中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置。然后它们被存储到了各个分片上了。

什么是节点和分片?

一个集群包含至少一个节点,而一个节点就是一个elasticsearch进程。节点内可以有多个索引。
默认的,如果你创建一个索引,那么这个索引将会有5个分片(primary shard,又称主分片)构成,而每个分片又有一个副本(replica shard,又称复制分片),这样,就有了10个分片。如下图

上图是一个有3个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某个节点挂掉了,数据也不至于丢失。
实际上,一个分片是一个Lucene索引,一个包含倒排索引的文件目录,倒排索引的结构使得elasticsearch在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字。

倒排索引

elasticsearch使用的是一种称为倒排索引的结构,采用Lucene倒排索作为底层。elasticsearch将索引被分为多个分片,每份分片是一个Lucene的索引。所以一个elasticsearch索引是由多个Lucene索引组成的。这种结构适用于快速的全文搜索,一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。

如下表,进一步理解倒排索引:

学科(原始数据) 索引列表(倒排索引)
id 学科 语言 id
1 语文 语文 1,2,3
2 语文 数学 3,4
3 语文,数学    
4 数学    

MySQL,MongoDB,Elasticsearch对比

关系型数据库和非关系型数据库比对
MySQL MongoDB Elasticsearch
数据库(database) 数据库 索引(indices)
表(tables) 表(Collection) types
行(rows)  Documents Documents
字段(columns) Field Field

 

总结:MySQL为关系型数据库,而MongoDB,Elasticsearch为非关系型数据库,由此看出两者的特点比较明显;

elasticsearch(集群)中可以包含多个索引(数据库),每个索引中可以包含多个类型(表),每个类型下又包含多个文档(行),每个文档中又包含多个字段(列)。

物理设计 ---> elasticsearch后台是如何处理这些数据的呢?elasticsearch将每个索引划分为多个分片,每份分片又可以在集群中的不同服务器间迁移。

注意:当然,这里需要补充的是,从elasticsearch的第一个版本开始,每个文档都存储在一个索引中,并分配多个映射类型,映射类型用于表示被索引的文档或者实体的类型,但这也带来了一些问题(详情参见Removal of mapping types),导致后来在elasticsearch6.0.0版本中一个文档只能包含一个映射类型,而在7.0.0中,映射类型则将被弃用,到了8.0.0中则将完全被删除。

elasticsearch的语法

1.基本语法写法

#基本语法
#PUT创建一文档(注意大写)t1的文档
PUT t1/doc/1
{
  "name":"可可"
}

#查询t1的文档索引信息
GET t1/doc/1  
#删除索引
DELETE t1/doc/1

PUT t1/doc/2 
{
  "name":"盼盼",
  "age":20
}

GET t1    #返回t1所有的创建信息
GET _cat/indices   #查询所有文档的信息
GET t1/_settings   #查询t1的主分片和复制分片情况
GET t1/_mapping  #查看t1的映射关系结构

 

创建一些数据来操作操作

#是不是有种来到汉朝的感觉,没事,放松点,呵呵
PUT han/doc/1
{
    "name":"孝景帝",
    "from":"汉朝",
    "in_time":16,
    "info":"无为而治",
    "tags":["平定七国之乱","诛晁错"],
    "female":"窦太后"  
}
PUT han/doc/2
{
    "name":"孝武帝",
    "from":"汉朝",
    "in_time":54,
    "info":"以战养战",
    "tags":["罢黜百家,独尊儒术","推恩令","平定北方匈奴","丝绸之路"],
    "female":"卫子夫"  
}

PUT han/doc/3
{
    "name":"汉高祖",
    "from":"汉朝",
    "in_time":7,
    "info":"建立汉朝",
    "tags":["鸿门宴","灭秦,楚国"],
    "female":"吕雉"  
}

PUT han/doc/4
{
    "name":"卫青",
    "from":"孝武帝",
    "in_time":2,
    "title":"关内侯",
    "info":"初伐匈奴",
    "tags":["奇袭龙城"]
}

PUT han/doc/5
{
    "name":"李广",
    "in_time":1,
    "from":"孝武帝",
    "title":"飞将军",
    "info":"射石搏虎",
    "tags":["平七国吴楚联军"]
}

PUT han/doc/6
{
    "name":"霍去病",
    "from":"孝武帝",
    "in_time":3,
    "title":"骠骑校尉",
    "info":"英年早逝,封景桓侯",
    "tags":["八百铁骑,直插敌后","平定朔方,打通西域"]
}

PUT han/doc/7
{
    "name":"李广利",
    "from":"孝武帝",
    "title":"贰师将军",
    "in_time":4,
    "info":"二征大宛",
    "tags":["征服大宛"]
}
创建数据

2.结构化查询(match相关)

"""match按关键字查询"""
#match_all查询全部
GET han/doc/_search
{
  "query": {
    "match_all": {}
  }
}
#match指定字段查询,但是这样会查出来所有name字段中跟孝武帝相关的数据,列如孝景帝. GET han/doc/_search { "query": { "match": { "name":"孝武帝" } } } #单独查询某条数据match_phrase,只会查到name是'孝武帝'的数据 GET han/doc/_search { "query": { "match_phrase": { "name": "孝武帝" } } } #最左前缀查询,match_phrase_prefix,不记得字段信息了,智能匹配 GET han/doc/_search { "query": { "match_phrase_prefix": { "name": "" } } }
#多字段查询multi_match ,查到字段中含有"帝"的数据 GET han/doc/_search { "query": { "multi_match": { "query": "", "fields": ["name","title"] } } } #multi_match甚至可以match_phrase,match_phrase_prefix使用,只要指定type即可 GET han/doc/_search { "query": { "multi_match": { "query": "", "fields": ["name"], "type": "phrase" } } } GET han/doc/_search { "query": { "multi_match": { "query": "", "fields": ["name"], "type": "phrase_prefix" } } }
#排序sort

#降序
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "sort":[
    {
      "in_time":{
        "order":"desc"
      }
    }
    ]
}

#升序
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "in_time": {
        "order": "asc"
      }
    }
  ]
}
排序语法
#过滤_source
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "in_time": {
        "order": "asc"
      }
    }
  ],
  "_source": [ "from", "in_time","name"]
}
过滤 _source
#分页 from从哪开始,size:展示条数,from,size也可以为-1
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "from":2,
  "size":2,
  "_source": "in_time"
}
分页
#高亮 highlight
GET han/doc/_search
{
  "query": {
    "match": {
      "from":"孝武帝"
    }
  },
  "highlight":{
    "pre_tags":"<b style='color:red;'>",
    "post_tags":"</b>",
    "fields":{
      "from": {}
    }
  }
}
搜素信息 高亮
#bool查询:must(and)/should(or)/must_not(not)

#and
GET han/doc/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "in_time": "1"
          }
        },
        {
        "match": {
          "from": "孝武帝"
          }  
        }
      ]
    }
  }
}

#or
GET han/doc/_search
{
  "query": {
    "bool": {
      "should": [
        {"match": {
          "from": "汉朝"
          }
        },
        {
        "match": {
          "in_time": 54
          }  
        }
      ]
    }
  }
}

#not
GET han/doc/_search
{
  "query": {
    "bool": {
      "must_not": [
        {"match": {
          "from": "孝武帝"
          }
        },
        {
        "match": {
          "in_time": 54
          }  
        }
      ]
    }
  }
}
bool查询:must,should,must_not
#查询filter:gte(大于等于) gt(大于)lt(小于)lte(小于等于)

GET han/doc/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "from": "汉朝"
          }
        }
      ],
      "filter": {
        "range": {
          "in_time": {
            "gte": 10
          }
        }
      }
    }
  }
}

#小于
GET han/doc/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "from": "汉朝"
          }
        }
      ],
      "filter": {
        "range": {
          "in_time": {
            "lt": 10
          }
        }
      }
    }
  }
}
filter查询lte,lt,gt,gte
#聚合查询:avg,sum,max,min
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "my_a1": {
      "avg": {
        "field": "in_time"
      }
    }
  }
}
聚合查询 aggs
#分组
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "my_group": {
      "range": {
        "field": "in_time",
        "ranges": [
          {
            "from": 20,
            "to": 60
          }
        ]
      }
    }
  }
}

#分组再聚合
GET han/doc/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "my_group": {
      "range": {
        "field": "in_time",
        "ranges": [
          {
            "from": 10,
            "to": 60
          }
        ]
      },
      "aggs": {
        "my_sum": {
          "sum": {
            "field": "in_time"
          }
        }
      }
    }
  }
}
分组查询,分组再聚合

3.映射

映射用来自定义一个文档及其包含的字段如何存储和索引的过程

例如,我们可以使用映射来定义:

  • 哪些字符串应该被视为全文字段。
  • 哪些字段包含数字、日期或者地理位置。
  • 定义日期的格式。
  • 自定义的规则,用来控制动态添加字段的的映射。
#映射mappings写法-01 dynamic没写默认为true,可以添加字段
PUT t3
{
  "mappings":{
    "doc":{
      "properties":{
        "name":{
          "type":"text"
        },
        "age":{
          "type":"long"
        }
      }
    }
  }
  
}

GET t3/_mapping

PUT t3/doc/1
{
  "name":"窦颖",
  "age":17,
  "desc":"可爱"
}

GET t3/doc/_search
{
  "query": {
    "match": {
      "age": 17
    }
  }
}

#mappings写法-02 dynamic=false 不让添加字段,但不报错
PUT t4
{
  "mappings":{
    "doc":{
      "dynamic":false,
      "properties":{
        "name":{
          "type":"text"
        },
        "age":{
          "type":"long"
        }
      }
    }
  }
  
}

GET t4/_mapping

PUT t4/doc/1
{
  "name":"",
  "age":19,
  "desc":"可ke爱"
}


#mappings写法-03 dynamic=strict 严格模式,只要添加就报错
PUT t5
{
  "mappings": {
    "doc":{
      "dynamic":"strict",
      "properties":{
        "name":{
          "type":"text"
        }
      }
    }
  }
}

GET t5/_mapping

PUT t5/doc/1
{
  "name":"coco"
}

#缺省/严格的时候不能添加字段
PUT t5/doc/2
{
  "name":"cici",
  "age":9
}
mappings自定义形式和dynamic的三种形态
#index属性,类似于模糊查询配置,有两种状态,当true,我们的自定义字段可以实现模糊查询,false则会报错

PUT t6
{
  "mappings": {
    "doc":{
      "properties":{
        "title":{
          "type":"text",
          "index":true
        },
        "content":{
          "type":"text",
          "index":false
        }
      }
    }
  }
}


PUT t6/doc/1
{
  "title":"es长路漫漫",
  "content":"py唯你作伴"
}

#报错
GET t6/doc/_search
{
  "query": {
    "match": {
      "title":"漫漫"
    }
  }
}

GET t6/doc/_search
{
  "query": {
    "match": {
      "content":"py"
    }
  }
}
mappings写法02 index
#copy_to,该属性允许我们将多个字段的值复制到组字段中,然后将组字段作为单个字段进行查询。
#copy_to写法-01
PUT t7
{
  "mappings": {
    "doc":{
      "properties":{
        "title":{
          "type":"text",
          "copy_to":"full_name"
        },
        "content":{
          "type":"text",
          "copy_to":"full_name"
        },
        "full_name":{
          "type":"text"
        }
      }
    }
  }
}

PUT t7/doc/1
{
  "title":"aa",
  "content":"bb"
}

GET t7/doc/_search
{
  "query": {
    "match": {
      "full_name": "bb"
    }
  }
}

#copy_to写法-02
PUT t8
{
  "mappings": {
    "doc":{
      "properties":{
        "title":{
          "type":"text",
          "copy_to":["full_name1","full_name2"]
        },
        "content":{
          "type":"text",
          "copy_to":"full_name"
        },
        "full_name1":{
          "type":"text"
        },
        "full_name2":{
          "type":"text"
        }
      }
    }
  }
}
mappings写法03 copy_to
#对象属性,mappings中嵌套结构写法,查询方式:info.addr
PUT t9
{
  "mappings": {
    "doc":{
      "dynamic":false,
      "properties":{
        "name":{
          "type":"text"
        },
        "age":{
          "type":"long"
        },
        "info":{
          "properties":{
            "addr":{
              "type":"text"
            },
            "tel":{
              "type":"text"
            }
          }
        }
      }   
  }
}
}


PUT t9/doc/1
{
  "name":"vov",
  "age":19,
  "info":{
    "addr":"earth",
    "tel":"no"
  }
}
  
GET t9/doc/_search
{
  "query": {
    "match": {
      "info.addr": "earth"
    }
  }
}
mappings写法04 对象属性
#settings设置,设置主,复制分片.
#number_of_shards主分片数量,默认5,number_of_replicas复制分片,默认1个

PUT w1
{
  "mappings": {
    "doc":{
      "properties":{
        "title":{
          "type":"text"
        }
      }
    }
  },
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 2
    
  }
}


GET w1
mappings写法05 settings

 

...待续

posted @ 2019-04-25 21:21  FindSoul  阅读(194)  评论(0编辑  收藏  举报
var RENDERER = { POINT_INTERVAL : 5, FISH_COUNT : 3, MAX_INTERVAL_COUNT : 50, INIT_HEIGHT_RATE : 0.5, THRESHOLD : 50, init : function(){ this.setParameters(); this.reconstructMethods(); this.setup(); this.bindEvent(); this.render(); }, setParameters : function(){ this.$window = $(window); this.$container = $('#jsi-flying-fish-container'); this.$canvas = $('
'); this.context = this.$canvas.appendTo(this.$container).get(0).getContext('2d-disabled'); this.points = []; this.fishes = []; this.watchIds = []; }, createSurfacePoints : function(){ var count = Math.round(this.width / this.POINT_INTERVAL); this.pointInterval = this.width / (count - 1); this.points.push(new SURFACE_POINT(this, 0)); for(var i = 1; i < count; i++){ var point = new SURFACE_POINT(this, i * this.pointInterval), previous = this.points[i - 1]; point.setPreviousPoint(previous); previous.setNextPoint(point); this.points.push(point); } }, reconstructMethods : function(){ this.watchWindowSize = this.watchWindowSize.bind(this); this.jdugeToStopResize = this.jdugeToStopResize.bind(this); this.startEpicenter = this.startEpicenter.bind(this); this.moveEpicenter = this.moveEpicenter.bind(this); this.reverseVertical = this.reverseVertical.bind(this); this.render = this.render.bind(this); }, setup : function(){ this.points.length = 0; this.fishes.length = 0; this.watchIds.length = 0; this.intervalCount = this.MAX_INTERVAL_COUNT; this.width = this.$container.width(); this.height = this.$container.height(); this.fishCount = this.FISH_COUNT * this.width / 500 * this.height / 500; this.$canvas.attr({width : this.width, height : this.height}); this.reverse = false; this.fishes.push(new FISH(this)); this.createSurfacePoints(); }, watchWindowSize : function(){ this.clearTimer(); this.tmpWidth = this.$window.width(); this.tmpHeight = this.$window.height(); this.watchIds.push(setTimeout(this.jdugeToStopResize, this.WATCH_INTERVAL)); }, clearTimer : function(){ while(this.watchIds.length > 0){ clearTimeout(this.watchIds.pop()); } }, jdugeToStopResize : function(){ var width = this.$window.width(), height = this.$window.height(), stopped = (width == this.tmpWidth && height == this.tmpHeight); this.tmpWidth = width; this.tmpHeight = height; if(stopped){ this.setup(); } }, bindEvent : function(){ this.$window.on('resize', this.watchWindowSize); this.$container.on('mouseenter', this.startEpicenter); this.$container.on('mousemove', this.moveEpicenter); this.$container.on('click', this.reverseVertical); }, getAxis : function(event){ var offset = this.$container.offset(); return { x : event.clientX - offset.left + this.$window.scrollLeft(), y : event.clientY - offset.top + this.$window.scrollTop() }; }, startEpicenter : function(event){ this.axis = this.getAxis(event); }, moveEpicenter : function(event){ var axis = this.getAxis(event); if(!this.axis){ this.axis = axis; } this.generateEpicenter(axis.x, axis.y, axis.y - this.axis.y); this.axis = axis; }, generateEpicenter : function(x, y, velocity){ if(y < this.height / 2 - this.THRESHOLD || y > this.height / 2 + this.THRESHOLD){ return; } var index = Math.round(x / this.pointInterval); if(index < 0 || index >= this.points.length){ return; } this.points[index].interfere(y, velocity); }, reverseVertical : function(){ this.reverse = !this.reverse; for(var i = 0, count = this.fishes.length; i < count; i++){ this.fishes[i].reverseVertical(); } }, controlStatus : function(){ for(var i = 0, count = this.points.length; i < count; i++){ this.points[i].updateSelf(); } for(var i = 0, count = this.points.length; i < count; i++){ this.points[i].updateNeighbors(); } if(this.fishes.length < this.fishCount){ if(--this.intervalCount == 0){ this.intervalCount = this.MAX_INTERVAL_COUNT; this.fishes.push(new FISH(this)); } } }, render : function(){ requestAnimationFrame(this.render); this.controlStatus(); this.context.clearRect(0, 0, this.width, this.height); this.context.fillStyle = 'hsl(0, 0%, 95%)'; for(var i = 0, count = this.fishes.length; i < count; i++){ this.fishes[i].render(this.context); } this.context.save(); this.context.globalCompositeOperation = 'xor'; this.context.beginPath(); this.context.moveTo(0, this.reverse ? 0 : this.height); for(var i = 0, count = this.points.length; i < count; i++){ this.points[i].render(this.context); } this.context.lineTo(this.width, this.reverse ? 0 : this.height); this.context.closePath(); this.context.fill(); this.context.restore(); } }; var SURFACE_POINT = function(renderer, x){ this.renderer = renderer; this.x = x; this.init(); }; SURFACE_POINT.prototype = { SPRING_CONSTANT : 0.03, SPRING_FRICTION : 0.9, WAVE_SPREAD : 0.3, ACCELARATION_RATE : 0.01, init : function(){ this.initHeight = this.renderer.height * this.renderer.INIT_HEIGHT_RATE; this.height = this.initHeight; this.fy = 0; this.force = {previous : 0, next : 0}; }, setPreviousPoint : function(previous){ this.previous = previous; }, setNextPoint : function(next){ this.next = next; }, interfere : function(y, velocity){ this.fy = this.renderer.height * this.ACCELARATION_RATE * ((this.renderer.height - this.height - y) >= 0 ? -1 : 1) * Math.abs(velocity); }, updateSelf : function(){ this.fy += this.SPRING_CONSTANT * (this.initHeight - this.height); this.fy *= this.SPRING_FRICTION; this.height += this.fy; }, updateNeighbors : function(){ if(this.previous){ this.force.previous = this.WAVE_SPREAD * (this.height - this.previous.height); } if(this.next){ this.force.next = this.WAVE_SPREAD * (this.height - this.next.height); } }, render : function(context){ if(this.previous){ this.previous.height += this.force.previous; this.previous.fy += this.force.previous; } if(this.next){ this.next.height += this.force.next; this.next.fy += this.force.next; } context.lineTo(this.x, this.renderer.height - this.height); } }; var FISH = function(renderer){ this.renderer = renderer; this.init(); }; FISH.prototype = { GRAVITY : 0.4, init : function(){ this.direction = Math.random() < 0.5; this.x = this.direction ? (this.renderer.width + this.renderer.THRESHOLD) : -this.renderer.THRESHOLD; this.previousY = this.y; this.vx = this.getRandomValue(4, 10) * (this.direction ? -1 : 1); if(this.renderer.reverse){ this.y = this.getRandomValue(this.renderer.height * 1 / 10, this.renderer.height * 4 / 10); this.vy = this.getRandomValue(2, 5); this.ay = this.getRandomValue(0.05, 0.2); }else{ this.y = this.getRandomValue(this.renderer.height * 6 / 10, this.renderer.height * 9 / 10); this.vy = this.getRandomValue(-5, -2); this.ay = this.getRandomValue(-0.2, -0.05); } this.isOut = false; this.theta = 0; this.phi = 0; }, getRandomValue : function(min, max){ return min + (max - min) * Math.random(); }, reverseVertical : function(){ this.isOut = !this.isOut; this.ay *= -1; }, controlStatus : function(context){ this.previousY = this.y; this.x += this.vx; this.y += this.vy; this.vy += this.ay; if(this.renderer.reverse){ if(this.y > this.renderer.height * this.renderer.INIT_HEIGHT_RATE){ this.vy -= this.GRAVITY; this.isOut = true; }else{ if(this.isOut){ this.ay = this.getRandomValue(0.05, 0.2); } this.isOut = false; } }else{ if(this.y < this.renderer.height * this.renderer.INIT_HEIGHT_RATE){ this.vy += this.GRAVITY; this.isOut = true; }else{ if(this.isOut){ this.ay = this.getRandomValue(-0.2, -0.05); } this.isOut = false; } } if(!this.isOut){ this.theta += Math.PI / 20; this.theta %= Math.PI * 2; this.phi += Math.PI / 30; this.phi %= Math.PI * 2; } this.renderer.generateEpicenter(this.x + (this.direction ? -1 : 1) * this.renderer.THRESHOLD, this.y, this.y - this.previousY); if(this.vx > 0 && this.x > this.renderer.width + this.renderer.THRESHOLD || this.vx < 0 && this.x < -this.renderer.THRESHOLD){ this.init(); } }, render : function(context){ context.save(); context.translate(this.x, this.y); context.rotate(Math.PI + Math.atan2(this.vy, this.vx)); context.scale(1, this.direction ? 1 : -1); context.beginPath(); context.moveTo(-30, 0); context.bezierCurveTo(-20, 15, 15, 10, 40, 0); context.bezierCurveTo(15, -10, -20, -15, -30, 0); context.fill(); context.save(); context.translate(40, 0); context.scale(0.9 + 0.2 * Math.sin(this.theta), 1); context.beginPath(); context.moveTo(0, 0); context.quadraticCurveTo(5, 10, 20, 8); context.quadraticCurveTo(12, 5, 10, 0); context.quadraticCurveTo(12, -5, 20, -8); context.quadraticCurveTo(5, -10, 0, 0); context.fill(); context.restore(); context.save(); context.translate(-3, 0); context.rotate((Math.PI / 3 + Math.PI / 10 * Math.sin(this.phi)) * (this.renderer.reverse ? -1 : 1)); context.beginPath(); if(this.renderer.reverse){ context.moveTo(5, 0); context.bezierCurveTo(10, 10, 10, 30, 0, 40); context.bezierCurveTo(-12, 25, -8, 10, 0, 0); }else{ context.moveTo(-5, 0); context.bezierCurveTo(-10, -10, -10, -30, 0, -40); context.bezierCurveTo(12, -25, 8, -10, 0, 0); } context.closePath(); context.fill(); context.restore(); context.restore(); this.controlStatus(context); } }; $(function(){ RENDERER.init(); });