一个简单的PHP模板引擎
PHP早期开发中通常是PHP代码和HTML代码混写,这也使代码中充斥着数据库操作,逻辑处理等。当项目不大时,这样的代码还可以接受,但是随着项目不断扩大,我们就会发现同一个文件中同时存在前端逻辑和后端处理,当逻辑越来越复杂时,代码的可读性和可维护性都会变得非常差,以至于后来不得不进行大规模的代码重构。所以后来就出现了代码分层的思想,尽量拆分开前端代码和后端代码。
PHP模板引擎能解决这种混乱吗?当然可以。但是呢,即使你不用专门的模板引擎也可以写出逻辑清晰的代码,重点是要有分层的思想,有专门的脚本去进行数据获取,数据处理,逻辑处理等,在展示页面只进行尽可能简单的逻辑处理即可。既然这样,那还有使用PHP模板引擎的必要吗?毫无疑问当然有,因为模板引擎的功能不仅于此。
那接下来就说一下PHP模板引擎的主要作用:
1、它实现了一些自定义标签,用于展示层的简单逻辑处理,相较于不适用引擎的好处是代码看起来不像是PHP代码了,感觉上HTML代码和PHP代码完全分开了,但这只是假象,坏处是效率降低了,因为这样的页面需要专门的脚本解析后才能正确显示,解析的方法就是使用正则表达式替换,这明显降低了效率。到现在来看感觉PHP模板引擎有还不如没有呢,那为什么还要用它呢,重点是他的下一个功能。
2、文件缓存,这是模板引擎在生产环境中提高效率的非常好的手段。可以用在页面加载时所用数据量很大但不经常变或者不需要实时更新,即使延迟一会也无妨的页面。我个人感觉文件缓存是PHP模板引擎的最重要的部分。
接下来我们就写一个简易的模板引擎(最后有完整文件代码)
首先我们先要计划好我们的所需要的基础类,有Template类和Compile类。
在我们具体实现编译功能之前先搭好一个空的骨架,具体如下:
<?php /**
* 模板引擎基础类
*/
class Template
{
private $config = array(
'suffix' => '.m', // 设置模板文件的后缀
'templateDir' => 'template/', // 设置模板所在的文件夹
'compileDir' => 'cache/', // 设置编译后存放的目录
'cache_html' => true, // 是否需要编译成静态的HTML文件
'suffix_cache' => '.html', // 设置编译文件的后缀
'cache_time' => 7200, // 多长时间自动更新,单位秒
'php_turn' => true, // 是否支持原生PHP代码
'cache_control' => 'control.dat',
'debug' => false,
);
private static $instance = null;
private $value = array(); // 值栈
private $compileTool; // 编译器
public $file; // 模板文件名,不带路径
public $debug = array(); // 调试信息
private $controlData = array();
public function __construct($config = array())
{
$this->debug['begin'] = microtime(true);
$this->config = $config + $this->config;
if (! is_dir($this->config['templateDir'])) {
exit("模板目录不存在!");
}
if (! is_dir($this->config['compileDir'])) {
mkdir($this->config['compileDir'], 0770);
}
$this->getPath();
include './Compile.php';
}
/**
*获取绝对路径
*/
public function getPath() {
$this->config['templateDir'] = strtr(realpath($this->config['templateDir']), '\\', '/').'/';
$this->config['compileDir'] = strtr(realpath($this->config['compileDir']), '\\', '/').'/';
}
/**
*取得模板引擎的实例
*/
public static function getInstance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/* 设置模板引擎参数 */
public function setConfig($key, $value = null) {
if (is_array($key)) {
$this->config = $key + $this->config;
}else {
$this->config[$key] = $value;
}
}
/* 获取当前模板引擎配置,仅供调试使用 */
public function getConfig($key = null) {
if ($key) {
return $this->config[$key];
}else {
return $this->config;
}
}
/**
*注入单个变量
*/
public function assign($key, $value) {
$this->value[$key] = $value;
}
/**
*注入数组变量
*/
public function assignArray($array) {
if (is_array($array)) {
foreach($array as $k => $v) {
$this->value[$k] = $v;
}
}
}
/**
* 获取模板文件完整路径
*/
public function path() {
return $this->config['templateDir'].$this->file.$this->config['suffix'];
}
/**
*判断是否开启了缓存
*/
public function needCache() {
return $this->config['cache_html'];
}
/**
*显示模板
*/
public function show($file) { }
} ?>
从上边的代码中我们能发现对于模板文件不存在和编译文件不存在处理方式不同,这也很容易理解,如果你连模板文件都没有有啥好编译的,但是你有模板文件没编译文件这也很正常,正常进行编译即可。
如上所示,我们首先想好了这个模板引擎需要什么配置,还有一些设置配置的方法和检查配置的方法等,而我们的核心方法show()还没有实现呢,先不着急,我们先去写编译类Compile,如下所示:
<?php class Compile
{
private $template; // 待编译的文件
private $content; // 需要替换的文件
private $comfile; // 编译后的文件
private $left = '{'; // 左定界符
private $right = '}'; // 右定界符
private $value = array(); // 值栈
private $phpTurn;
private $T_P = array(); // 匹配正则数组
private $T_R = array(); // 替换数组
public function __construct($template, $compileFile, $config) {
$this->template = $template;
$this->comfile = $compileFile;
$this->content = file_get_contents($template);
}
public function compile() {
$this->c_var();
file_put_contents($this->comfile, $this->content);
}
public function c_var() {
$this->content = preg_replace($this->T_P, $this->T_R, $this->content);
}
public function __set($name, $value) {
$this->$name = $value;
}
public function __get($name) {
return $this->$name;
}
} ?>
从上面Compile类的构造函数我们可以看出,他需要模板文件路径,编译文件路径,和具体编译时的配置参数,但是在这这个配置参数吗,没有用到。之前说过模板引擎主要使用的正则表达式来进行匹配替换,将模板文件编译成能正确执行的PHP文件,这里使用数组存放正则匹配数组和替换数组来进行整体替换。
接下来我们就简单实现几个常用的标签,先看怎么替换简单变量:
// \x7f-\xff表示ASCII字符从127到255,其中\为转义,作用是匹配汉字
$this->T_P[] = "#\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}#";
$this->T_R[] = "<?php echo \$this->value['\\1']; ?>";
正如我们看到的,上边的那个是正则匹配,下边的是替换。但是我们没有给编译类的value变量赋值,那这个替换能找到正确的值吗?答案是能,因为他用的不是这个类的value用的是模板类的value,接下来一会会讲到。
然后我们在看看怎么实现foreach标签,这个很常用
$this->T_P[] = "#\{(loop|foreach)\s+\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\}#i";
$this->T_P[] = "#\{\/(loop|foreach)\}#";
$this->T_R[] = "<?php foreach ((array)\$this->value['\\2'] as \$k => \$v) { ?>";
$this->T_R[] = "<?php } ?>";
这里用到的主要正则表达式的知识有:元组、反向引用等,这些只要稍微看一下正则表达式基础就能理解了。
我们再来一个if else标签:
$this->T_P[] = "#\{\/(loop|foreach|if)\}#";
$this->T_P[] = "#\{if (.*?)\}#";
$this->T_P[] = "#\{(else if|elseif) (.*?)\}#";
$this->T_P[] = "#\{else\}#";
$this->T_R[] = "<?php } ?>";
$this->T_R[] = "<?php if(\\1){ ?>";
$this->T_R[] = "<?php }elseif(\\2){ ?>";
$this->T_R[] = "<?php }else{ ?>";
我们将if的闭合标签和foreach的闭合标签放一块了。
现在我们已经能编译一些标签了我们就再转回模板类,现在我们想一想要怎么展示呢,这个才是我们的根本目的。写代码之前先理一下思路:
1、判断是否开启了缓存,如果是进行第二步,否则直接进行编译输出。
2、判断是否需要更新缓存(判断方式主要是缓存时间和编译文件和模板文件的修改时间的关系),如果是就进行第三步,否则直接读取缓存文件输出。
3、重新编译模板文件,并将编译后的PHP文件输出保存到静态缓存文件中。
简单来说就是上边的那三个步骤,具体实现如下:
/**
*是否需要重新生成静态文件
*/
public function reCache($file) {
$flag = true;
$cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache'];
if ($this->config['cache_html'] === true) {
$timeFlag = (time() - @filemtime($cacheFile)) < $this->config['cache_time'] ? true : false;
if (is_file($cacheFile) && filesize($cacheFile) > 1 && $timeFlag && filemtime($compileFile) >= filemtime($this->path())) {
$flag = false;
}else {
$flag = true;
}
}
return $flag;
}
/**
*显示模板
*/
public function show($file) {
$this->file = $file;
if (! is_file($this->path())) {
exit('找不到对应的模板!');
}
$compileFile = $this->config['compileDir'].md5($file).'.php';
$cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache'];
extract($this->value, EXTR_OVERWRITE);
if ($this->config['cache_html'] === true) {
if ($this->reCache($file) === true) {
$this->debug['cached'] = 'false';
$this->compileTool = new Compile($this->path(), $compileFile, $this->config);
if ($this->needCache()) {ob_start();}
if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) {
$this->compileTool->value = $this->value;
$this->compileTool->compile();
include $compileFile;
}else {
include $compileFile;
}
if ($this->needCache()) {
$message = ob_get_contents();
file_put_contents($cacheFile, $message);
}
}else {
readfile($cacheFile);
$this->debug['cached'] = 'true';
}
}else {
if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) {
$this->compileTool = new Compile($this->path(), $compileFile, $this->config);
$this->compileTool->value = $this->value;
$this->compileTool->compile();
include $compileFile;
}else {
include $compileFile;
}
}
$this->debug['spend'] = microtime(true) - $this->debug['begin'];
$this->debug['count'] = count($this->value);
}
上边的代码基本是按照上述的三个步骤来进行的,好好看一下不难理解。
接下来就是模板文件了:
<html>
{$data},{$person}
<br/>列表一:<br/>
<ul>
{foreach $arr1}
<li>$v</li>
{/foreach}
</ul>
<br/>列表二:<br/>
<ul>
{loop $arr2}
<li>$v</li>
{/loop}
</ul>
{if $a == '1'}a等于1
{elseif $a == '2'}a等于2
{else}a不等于1也不等于2
{/if}
</html>
这个模板文件主要测试了之前我们事先的模板标签。
下面写一个测试文件:
<?php include_once './Template.php';
$tpl = new Template();
$tpl->assign('data', 'hello');
$tpl->assign('person', 'world!');
$tpl->assign('arr1', array('123','456','789'));
$tpl->assign('arr2', array('abc', 'def', 'ghi'));
$tpl->assign('a', '2');
$tpl->show('member'); ?>
这就是一个简单的测试我们的模板引擎能不能用的测试用例。
下面我们看看完整代码吧:
<?php /**
* 模板引擎基础类
*/
class Template
{
private $config = array(
'suffix' => '.m', // 设置模板文件的后缀
'templateDir' => 'template/', // 设置模板所在的文件夹
'compileDir' => 'cache/', // 设置编译后存放的目录
'cache_html' => true, // 是否需要编译成静态的HTML文件
'suffix_cache' => '.html', // 设置编译文件的后缀
'cache_time' => 7200, // 多长时间自动更新,单位秒
'php_turn' => true, // 是否支持原生PHP代码
'cache_control' => 'control.dat',
'debug' => false,
);
private static $instance = null;
private $value = array(); // 值栈
private $compileTool; // 编译器
public $file; // 模板文件名,不带路径
public $debug = array(); // 调试信息
private $controlData = array();
public function __construct($config = array())
{
$this->debug['begin'] = microtime(true);
$this->config = $config + $this->config;
if (! is_dir($this->config['templateDir'])) {
exit("模板目录不存在!");
}
if (! is_dir($this->config['compileDir'])) {
mkdir($this->config['compileDir'], 0770);
}
$this->getPath();
include './Compile.php';
}
/**
*获取绝对路径
*/
public function getPath() {
$this->config['templateDir'] = strtr(realpath($this->config['templateDir']), '\\', '/').'/';
$this->config['compileDir'] = strtr(realpath($this->config['compileDir']), '\\', '/').'/';
}
/**
*取得模板引擎的实例
*/
public static function getInstance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/* 设置模板引擎参数 */
public function setConfig($key, $value = null) {
if (is_array($key)) {
$this->config = $key + $this->config;
}else {
$this->config[$key] = $value;
}
}
/* 获取当前模板引擎配置,仅供调试使用 */
public function getConfig($key = null) {
if ($key) {
return $this->config[$key];
}else {
return $this->config;
}
}
/**
*注入单个变量
*/
public function assign($key, $value) {
$this->value[$key] = $value;
}
/**
*注入数组变量
*/
public function assignArray($array) {
if (is_array($array)) {
foreach($array as $k => $v) {
$this->value[$k] = $v;
}
}
}
/**
* 获取模板文件完整路径
*/
public function path() {
return $this->config['templateDir'].$this->file.$this->config['suffix'];
}
/**
*判断是否开启了缓存
*/
public function needCache() {
return $this->config['cache_html'];
}
/**
*是否需要重新生成静态文件
*/
public function reCache($file) {
$flag = true;
$cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache'];
if ($this->config['cache_html'] === true) {
$timeFlag = (time() - @filemtime($cacheFile)) < $this->config['cache_time'] ? true : false;
if (is_file($cacheFile) && filesize($cacheFile) > 1 && $timeFlag && filemtime($compileFile) >= filemtime($this->path())) {
$flag = false;
}else {
$flag = true;
}
}
return $flag;
}
/**
*显示模板
*/
public function show($file) {
$this->file = $file;
if (! is_file($this->path())) {
exit('找不到对应的模板!');
}
$compileFile = $this->config['compileDir'].md5($file).'.php';
$cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache'];
extract($this->value, EXTR_OVERWRITE);
if ($this->config['cache_html'] === true) { // 开启缓存的处理逻辑
if ($this->reCache($file) === true) { // 需要更新缓存的处理逻辑
$this->debug['cached'] = 'false';
$this->compileTool = new Compile($this->path(), $compileFile, $this->config);
if ($this->needCache()) {ob_start();} // 打开输出控制缓冲
if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) {
$this->compileTool->value = $this->value;
$this->compileTool->compile();
include $compileFile;
}else {
include $compileFile;
}
if ($this->needCache()) {
$message = ob_get_contents(); // 获取输出缓冲中的内容(在include编译文件的时候就有输出了)
file_put_contents($cacheFile, $message);
}
}else {
readfile($cacheFile);
$this->debug['cached'] = 'true';
}
}else {
if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) {
$this->compileTool = new Compile($this->path(), $compileFile, $this->config);
$this->compileTool->value = $this->value;
$this->compileTool->compile();
include $compileFile;
}else {
include $compileFile;
}
}
$this->debug['spend'] = microtime(true) - $this->debug['begin'];
$this->debug['count'] = count($this->value);
//$this->debug_info();
}
public function debug_info() {
if ($this->config['debug'] === true) {
echo PHP_EOL, '---------debug info---------', PHP_EOL;
echo "程序运行日期:", date("Y-m-d H:i:s"), PHP_EOL;
echo "模板解析耗时:", $this->debug['spend'], '秒', PHP_EOL;
echo '模板包含标签数目:', $this->debug['count'], PHP_EOL;
echo '是否使用静态缓存:', $this->debug['cached'], PHP_EOL;
echo '模板引擎实例参数:', var_dump($this->getConfig());
}
}
/**
*清理缓存的HTML文件
*/
public function clean($path = null) {
if ($path === null) {
$path = $this->config['compileDir'];
$path = glob($path.'* '.$this->config['suffix_cache']);
}else {
$path = $this->config['compileDir'].md5($path).$this->config['suffix_cache'];
}
foreach((array)$path as $v) {
unlink($v);
}
}
} ?>
<?php class Compile
{
private $template; // 待编译的文件
private $content; // 需要替换的文件
private $comfile; // 编译后的文件
private $left = '{'; // 左定界符
private $right = '}'; // 右定界符
private $value = array(); // 值栈
private $phpTurn;
private $T_P = array(); // 匹配正则数组
private $T_R = array(); // 替换数组
public function __construct($template, $compileFile, $config) {
$this->template = $template;
$this->comfile = $compileFile;
$this->content = file_get_contents($template);
if ($config['php_turn'] === true) {
$this->T_P[] = "#<\?(=|php|)(.+?)\?#is";
$this->T_R[] = "<?\1\2?>";
}
// 变量匹配
// \x7f-\xff表示ASCII字符从127到255,其中\为转义,作用是匹配汉字
$this->T_P[] = "#\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}#";
// foreach标签盘匹配
$this->T_P[] = "#\{(loop|foreach)\s+\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\}#i";
$this->T_P[] = "#\{\/(loop|foreach|if)\}#";
$this->T_P[] = "#\{([k|v])\}#";
// if else标签匹配
$this->T_P[] = "#\{if (.*?)\}#";
$this->T_P[] = "#\{(else if|elseif) (.*?)\}#";
$this->T_P[] = "#\{else\}#";
$this->T_P[] = "#\{(\#|\*)(.*?)(\#|\*)\}#"; $this->T_R[] = "<?php echo \$this->value['\\1']; ?>";
$this->T_R[] = "<?php foreach ((array)\$this->value['\\2'] as \$k => \$v) { ?>";
$this->T_R[] = "<?php } ?>";
$this->T_R[] = "<?php echo \$\\1?>";
$this->T_R[] = "<?php if(\\1){ ?>";
$this->T_R[] = "<?php }elseif(\\2){ ?>";
$this->T_R[] = "<?php }else{ ?>";
$this->T_R[] = "";
}
public function compile() {
$this->c_var();
//$this->c_staticFile();
file_put_contents($this->comfile, $this->content);
}
public function c_var() {
$this->content = preg_replace($this->T_P, $this->T_R, $this->content);
}
/* 对引入的静态文件进行解析,应对浏览器缓存 */
public function c_staticFile() {
$this->content = preg_replace('#\{\!(.*?)\!\}#', '<script src=\1'.'?t='.time().'></script>', $this->content);
}
public function __set($name, $value) {
$this->$name = $value;
}
public function __get($name) {
return $this->$name;
}
} ?>
模板文件member.m代码:
<html>
{$data},{$person}
<br/>列表一:<br/>
<ul>
{foreach $arr1}
<li>$v</li>
{/foreach}
</ul>
<br/>列表二:<br/>
<ul>
{loop $arr2}
<li>$v</li>
{/loop}
</ul>
{if $a == '1'}a等于1
{elseif $a == '2'}a等于2
{else}a不等于1也不等于2
{/if}
</html>
测试用例:
<?php include_once './Template.php';
$tpl = new Template();
$tpl->assign('data', 'hello');
$tpl->assign('person', 'world!');
$tpl->assign('arr1', array('123','456','789'));
$tpl->assign('arr2', array('abc', 'def', 'ghi'));
$tpl->assign('a', '2');
$tpl->show('member'); ?>
本文内容大部分来自于《PHP核心技术与最佳实践》的第六章。
一个简单的PHP模板引擎的更多相关文章
- 写一个迷你版Smarty模板引擎,对认识模板引擎原理非常好(附代码)
前些时间在看创智博客韩顺平的Smarty模板引擎教程,再结合自己跟李炎恢第二季开发中CMS系统写的tpl模板引擎.今天就写一个迷你版的Smarty引擎,虽然说我并没有深入分析过Smarty的源码,但是 ...
- OpenCms JSP 模板开发——创建一个简单的JSP模板
OpenCms中的JSP模板就是一个普通的JSP页面,在特定的位置使用标签来包含内容,在这个的例子中,我们将要开发一个简单JSP模板,这个模板只是在内容(如<html>.<body& ...
- 一个简单地template模板
之前的项目中用到了artTemplate模板,感觉挺有意思,于是查看相关资料,自己动手写了个简单地template模板插件.虽然会有一些不足,但也是自己的一番心血.主体代码如下 /* * 一个简单地t ...
- 基于 Roslyn 实现一个简单的条件解析引擎
基于 Roslyn 实现一个简单的条件解析引擎 Intro 最近在做一个勋章的服务,我们想定义一些勋章的获取条件,满足条件之后就给用户颁发一个勋章,定义条件的时候会定义需要哪些参数,参数的类型,获取勋 ...
- Html+css 一个简单的网页模板
一个简单的网页模板,有导航.子菜单.banner部分 1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN&q ...
- Epii.js 一个极其简单的Js模板引擎
Epii.js 简约而不简单的Js模板引擎 Epii.js 简约而不简单的JavaScript模板引擎 # 特性 一个轻量级模板引擎,可快速实现数据与ui绑定(数据变动,UI自动变动),快速实现事件绑 ...
- 最简单的JavaScript模板引擎
在小公司待久了感觉自己的知识面很小,最近逛博客园和一些技术网站看大家在说JavaScript模版引擎的事儿,完全没有概念,网上一搜这是08年开始流行起来的...本来以为这是很高深的知识,后来在网上看到 ...
- 用 php 实现一个视图组件和模板引擎——基础
只要不是做后端接口开发和一些作为守护进程之类的服务器脚本,大多数时候都是在和浏览器打交道,因此合理组织并展现 html 标签是最为常见的工作.一般大家使用框架时,都会自带有一套视图组件和模板引擎. 我 ...
- JQuery简单实用的模板引擎
1.在html界面声明模板(注意type类型) <script id="tmplInvokeProvider" type="text/x-jquery-tmpl&q ...
随机推荐
- Gradle笔记——依赖管理基础
1. 什么是依赖管理 依赖管理可以分为两部分:一是依赖,即项目构建或运行时所需要的一些文件:二是发布,即构建完成后上传到某个地方. 1.1 依赖 大部分的项目都需要第三方库类或项目文件,这些文件就是项 ...
- PA 模块常用表2
SELECT * FROM pa_expenditure_items_all 项目支出 select *from pa_cost_distribution_lines_all 支出分配行 SELE ...
- PKU 3468 A Simple Problem with Integers
题目大意: 有N,M两个数 Q 表示查询, 后面两个数a b,表示查询从a 到b计算它们的和 C 表示增加 后面三个数a,b,c 表示从a开始,一直到b,每个数都增加c 除了查询要进行输出,增加不 ...
- XBMC源代码简析 5:视频播放器(dvdplayer)-解复用器(以ffmpeg为例)
XBMC分析系列文章: XBMC源代码分析 1:整体结构以及编译方法 XBMC源代码分析 2:Addons(皮肤Skin) XBMC源代码分析 3:核心部分(core)-综述 XBMC源代码分析 4: ...
- 开源库BaseRecyclerViewAdapterHelper
相信大家RecyclerView应该不会陌生,大多数开发者应该都使用上它了,它也是google推荐替换ListView的控件,但是用过它的同学应该都知道它在某些方面并没有ListView使用起来方便, ...
- E-JSON数据传输标准
简介 E-JSON的设计目标是使业务系统向浏览器端传递的JSON数据保持一致,容易被理解和处理,并兼顾传输的数据量.E-JSON依托于http协议(rfc2616)与JSON数据交换格式(rfc462 ...
- MySQL正则表达式初步
如果想要了解完整的MySQL手册, 请访问: MySQL 5.1参考手册 你还可以学习: MySQL学习精粹 我们知道,在SQL之中,可以用 like 这个谓词(表达式) 来进行模糊检索,并支持 %, ...
- Gradle 1.12用户指南翻译——第二十八章. Jetty 插件
其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://g ...
- 求剁手的分享,如何简单开发js图表
前段时间做的一个项目里需要用到js图表,在网上找了下,大概找到了highcharts.fusioncharts这些国外产品. 因为都收费,虽然有盗版,我也不敢用,万一被找上们来就砸锅卖铁了要.自己写j ...
- PS图层混合算法之五(饱和度,色相,颜色,亮度)
饱和度模式: HcScYc =HBSAYB 饱和度模式:是采用底色的亮度.色相以及绘图色的饱和度来创建最终色.如果绘图色的饱和度为0,则原图没有变化. 输出图像的饱和度为上层,色调和亮度保持为下层. ...