前言
原文: Building a parallax scrolling storytelling framework
作者:
Creative Bloq Staff( netmag )
发表于 2011-08-24
Stevan Živadinović
, 多平面横向卷轴网络漫画 Hobo Lobo of Hamelin 的幕后主要人物, 他向我们介绍了 Parallaxer 平台的开发过程, 并提供了如何将铅笔画变成透明背景资产的速成课.
-
所需知识: 中级
JavaScript
/ 扎实的 Photoshop 词汇 -
需要: Photoshop /
JQuery
/JQuery ScrollTo
插件 /JQuery Easing
插件 -
项目时间: 半小时到几个月不等
-
支持文件
读完 MS Paint Adventures 后, 我有了一个顿悟: 在当今时代, 将漫画限制在20世纪初胶印的范围几乎没有任何理由. 不需要墨水线条艺术, 或艺术竞争页面空间的手写语音气泡, 不需要将绘图打包到固定尺寸的网格中并以四的倍数打印它们: 这些都是我们不再需要的问题的解决方案. 因此, 我采用了
Scott McCloud
的无限画布, 并通过添加一些 JavaScript 来加大赌注. 结果就是 Hobo Lobo of Hamelin, 这是一个网格故事, 讲述了一座城市、 它的顾忌、一些老鼠、一只狼、他的木管乐器以及那些倒下的东西.
我想教你制作专属的视差器, 向你展示为其绘制资源的一种方法, 并最终激励你以自己的方向开拓互联网讲故事的前沿.
01. 视差器的诞生
视差是一种反直觉的伪造 3D 空间的手段. 当你使用简单的 50 毫米相机镜头观察周围时, 你会发现随着与物体的距离增加, 更大的物体可以进入你的视野. 大约半米处, 你可以看到一本完整的书. 大约三米处, 你可以看到整个的沙发. 从 20m 处就可以看到整栋房子. 直截了当.
然而, 视差与距离无关. 它涉及向量. 如果你通过上一个示例中的相机查看, 并向一侧迈出一米, 则书将完全离开你的视口, 你将看到大约三分之二的沙发, 而房子几乎不会移动. 这个东西用数学来描述是相当混乱的.
为了更好地构思巨大的全景应该如何表现, 让我们将舞台和所有视差层分成面板. 这解决了很多我们还不知道的问题. 其一, 我们不必使用难以完全掌握的单一全景图, 而是可以分段绘制故事, 并更好地预测各层之间的相互作用, 同时它们位于浏览器的视图中、靠近中心的位置窗口(如下图虚线所示)
加载页面时, 第一个面板的所有层将彼此居中且位于窗口的中心. 当向右滚动时, 图层将在面板之间的边界处对齐, 然后再次在第二个面板上居中. 这种感觉就像俯视一连串的走廊.
该图显示了视差空间的另一反直觉方面. 最远的层, 也就是距离相机示例应该适合房子的层, 在水平方向上是所有层中最窄的(垂直方向上该层将与其他层一样高, 出于易读性的目的, 图表显示它们较短). 为了绘制一个与最大的书一样大的房子, 我们可以将其放入最近的层中, 我们将不得不溢出到最远层的相邻面板中.
在 Flash 中伪造全景视差相对来说是微不足道的, 但 Flash 并不像以前那样性感. 另外, 完整的叙述更新起来会很麻烦, 而且不利于好奇的访客进行逆向工程, 正如我所希望的那样.
让我们为适合此图的视差器设置一个小支架. 我将使用 XHTML, 因为没有隐式需要 HTML5 提供的任何内容, 但你可以使用任何你感兴趣的 doctype. 让我们包含 JQuery 1.6.2
(截止撰写本文时的最新版本)、JQuery ScrollTo 1.4.2
(当时最新版本)和 JQuery Easing 1.3
(另一个虽然过时但好用的).
<div id="overflowControl">
<div id="layerSling">
<div id="layerA">
<div class="p">1a</div> 1a
<div class="p">2a</div> 2a
<div class="p">3a</div> 3a
<div class="p">4a</div> 4a
<div class="p">5a</div> 5a
<div class="p">6a</div> 6a
</div>
<div id="layerB">
<div class="p">1b</div> 1b
<div class="p">2b</div> 2b
<div class="p">3b</div> 3b
<div class="p">4b</div> 4b
<div class="p">5b</div> 5b
<div class="p">6b</div> 6b
</div>
<div id="layerC">
<div class="p">1c</div> 1c
<div class="p">2c</div> 2c
<div class="p">3c</div> 3c
<div class="p">4c</div> 4c
<div class="p">5c</div> 5c
<div class="p">6c</div> 6c
</div>
<div id="layerD">
<div class="p">1d</div> 1d
<div class="p">2d</div> 2d
<div class="p">3d</div> 3d
<div class="p">4d</div> 4d
<div class="p">5d</div> 5d
<div class="p">6d</div> 6d
</div>
<div id="layerE">
<div class="p">1e</div> 1e
<div class="p">2e</div> 2e
<div class="p">3e</div> 3e
<div class="p">4e</div> 4e
<div class="p">5e</div> 5e
<div class="p">6e</div> 6e
</div>
</div>
</div>
我们在这里使用五层, 理论上 Parallaxer 对它可以支持的层数没有限制. A 层将在后面, 而 E 层将在前面. 溢出控制对于预测舞台最右侧发生的不稳定行为是必要的; 如果没有它, 我们会将最后一个面板滚动到其垂直对齐轴上. 数字和字母分别代表面板和图层, 在这里只是作为辅助工具.
我们需要决定哪一层是静态的, 针对哪一层进行视差. 该层也将是叙述容器的父层.
我们还需要为静态层中的面板选择宽度, 以及其它层的比率: 面板相对于静态层的宽度. 我将 B 层作为主图层, 并设置 400 像素宽度. 我们还将其他层面板的比率设置为 0.75、1.25、1.5 和 1.75, 分别对应 300、500、600 和 700 像素. 这些是 Hobo Lobo 的尺寸. 我发现它们足够灵活、适合各种空间情况, 但它们完全取决于你.
<ul id="panelControl">
<li><a href="#c_1" title="Panel 1"><span>1</span></a></li>
<li><a href="#c_2" title="Panel 2"><span>2</span></a></li>
<li><a href="#c_3" title="Panel 3"><span>3</span></a></li>
<li><a href="#c_4" title="Panel 4"><span>4</span></a></li>
<li><a href="#c_5" title="Panel 5"><span>5</span></a></li>
<li><a href="#c_6" title="Panel 6"><span>6</span></a></li>
</ul>
<div id="overflowControl">
<div id="layerSling">
<div id="layerA">
<div class="p">1a</div>
<div class="p">2a</div>
<div class="p">3a</div>
<div class="p">4a</div>
<div class="p">5a</div>
<div class="p">6a</div>
</div>
<div id="layerB">
<div class="p" id="c_1">1b<div class="narrative">
<p>Bacon ipsum dolor sit amet anim jerky sirloin, brisket salami cillum jowl.</p>
</div>
</div>
<div class="p" id="c_2">2b<div class="narrative">
<p>Laboris occaecat ut dolore minim, non shankle laborum sausage boudin meatball shoulder.</p>
</div>
</div>
<div class="p" id="c_3">3b<div class="narrative">
<p>Pastrami shankle ad chuck, chicken in strip steak pariatur culpa ex fatback sunt incididunt exercitation
elit.</p>
</div>
</div>
<div class="p" id="c_4">4b<div class="narrative">
<p>Elit dolor labore in pork tempor tri-tip cillum tenderloin duis, eiusmod ut aliquip strip steak.</p>
</div>
</div>
<div class="p" id="c_5">5b<div class="narrative">
<p>Pancetta swine in dolore id laborum, cupidatat adipisicing mollit.</p>
</div>
</div>
<div class="p" id="c_6">6b<div class="narrative">
<p>Officia incididunt adipisicing pancetta, ut veniam spare ribs cillum tempor flank chuck ex consectetur.</p>
</div>
</div>
</div>
<div id="layerC">
<div class="p">1c</div>
<div class="p">2c</div>
<div class="p">3c</div>
<div class="p">4c</div>
<div class="p">5c</div>
<div class="p">6c</div>
</div>
<div id="layerD">
<div class="p">1d</div>
<div class="p">2d</div>
<div class="p">3d</div>
<div class="p">4d</div>
<div class="p">5d</div>
<div class="p">6d</div>
</div>
<div id="layerE">
<div class="p">1e</div>
<div class="p">2e</div>
<div class="p">3e</div>
<div class="p">4e</div>
<div class="p">5e</div>
<div class="p">6e</div>
</div>
</div>
</div>
请注意 B 层面板中的叙述性 div 以及包含与 B 层中的 id 相对应的面板链接的菜单. 这些将提供另一种滚动.
/* GENERAL and CLEANUP */
* {
margin: 0px;
padding: 0px;
}
img {
border: 0px;
vertical-align: bottom;
}
body {
background-color: #fff;
font-size: 14px;
font-family: Monaco, Courier, monospace;
}
/* PARALLAXER */
ul#panelControl {
position: fixed;
top: 13px;
left: 70px;
z-index: 999;
}
ul#panelControl li {
float: left;
display: block;
padding-right: 3px;
}
ul#panelControl li a {
display: block;
border-radius: 20px;
background-color: #000;
color: #fff;
width: 26px;
height: 26px;
text-align: center;
font-size: 20px;
line-height: 22px;
text-decoration: none;
}
ul#panelControl li a.clicked {
opacity: 0.3;
filter: alpha(opacity=30);
}
ul#panelControl li a:hover {
text-decoration: none;
opacity: 1;
filter: alpha(opacity=100);
background-color: #fff;
color: #000;
box-shadow: 0px 5px 5px #000;
}
ul#panelControl li a span {
display: block;
padding-top: 2px;
}
#overflowControl {
overflow: hidden;
position: relative;
height: 620px;
top: 90px;
}
#layerSling {}
#layerSling>div {
display: block;
position: absolute;
top: 0px;
left: 0;
}
#layerSling .p {
height: 400px;
float: left;
position: relative;
overflow: visible;
text-align: center;
}
#layerSling .p img {
display: block;
position: absolute;
top: 0;
left: 0;
}
#layerB .p {
width: 400px;
}
#layerB .p .narrative {
position: absolute;
top: 400px;
left: 0;
width: 360px;
padding: 20px;
text-align: left;
}
/* FLAIR */
#layerA .p {
box-shadow: inset 5px -50px 40px rgba(218, 74, 141, 0.9);
color: #942a5c;
}
#layerB .p {
box-shadow: inset 5px -40px 40px rgba(187, 135, 197, 0.9);
color: #86578f;
}
#layerC .p {
box-shadow: inset 5px -30px 40px rgba(165, 166, 216, 0.9);
color: #696aae;
}
#layerD .p {
box-shadow: inset 5px -20px 40px rgba(154, 206, 241, 0.9);
color: #4ca1d9;
}
#layerE .p {
box-shadow: inset 5px -10px 40px rgba(153, 227, 255, 0.9);
color: #3dc3f5;
}
我们从没有溢出的溢出控制开始, 有一个高度, 并将其放置在距窗口顶部一点点的位置. 现在将在 JavaScript
中定义所有面板的宽度. 但最终当我们完成实验后, 我们应该给一些内容添加 CSS
样式, 并让其他内容在服务端计算并添加到样式标签中, 用来恢复损失的性能. flair
的标记只是我们开发过程中的临时支架
所有设置完成后, 我们需要 JavaScript
来做一些事情:
- 我们需要视差对视差.
- 我们需要所有面板始终在窗口中间对齐.
- 当面板的边缘与所有层重叠时, 我们应关闭传出面板的叙述并展开传入的面板
- 同时我们应该更改面板控制按钮的样式, 以提供有关已访问哪些面板的视觉提示
- 我们需要面板控制按钮来设置平滑动画而不是跳转到给定的面板
function aParallax() {
var p = this;
p.panelWd = 400; // the width of the primary panel
p.otherLayers = [{ id: 'layerA', ratio: 0.75 }, { id: 'layerC', ratio: 1.25 }, { id: 'layerD', ratio: 1.5 }, { id: 'layerE', ratio: 1.75 }]; // all of the other panels
p.panelCount = $('#layerB .p').length; // the total number of panels
p.didScroll = false;
p.panelHovered = 0; // current panel being hovered; will change as soon as the scripts start firing
p.overflowControl = $('#overflowControl'); // we will be dealing with this one enough to warrant putting it in its own var
// all methods will go here as well as constructor code
$(window).resize(function () {
// here we'll add all the stuff we need to fire when the window is resized
});
$(window).scroll(function () {
p.didScroll = true;
});
setInterval(function () {
if (p.didScroll) {
p.didScroll = false;
// all things that are affected by scrolling get executed here
}
}, 30);
}
$(document).ready(function () {
// execute when all code loads
p = new aParallax();
});
这个类将包含所有变量和方法, 以免用奇怪的命名事物污染全局. 大多数函数都是 aParallax 类的方法. 为了简化代码, 我们定义 p 以在类中解析此问题, 然后命名工作视差对象 p, 方便从类内部和外部对方法和变量的调用看起来相同.
我们还为我们需要做出反应的事件设立了几个区域. 请注意 setInterval 函数每 30 毫秒触发一次并连接到滚动事件. 不同的浏览器以不同的方式触发 $(window).scroll()
事件, 并且在滚动的每个像素上执行大量代码可能会导致事情陷入困境. 仅允许每隔 30 毫秒进行一次滚动重新计算, 从而缓解了这一问题.
让我们设置基本方法:
this.crunchWinVars = function () {
p.winWd = $(window).width();
p.winHoriSp2Panel = Math.floor((p.winWd - p.panelWd) / 2);
$('#layerSling').offset({ left: p.winHoriSp2Panel });
p.overflowControl.width(p.winWd + (p.panelCount - 1) * p.panelWd);
};
this.init = function () {
$('#layerB').width(p.panelCount * p.panelWd); // This might be better to bake into style tags serverside
for (var ih = 0; ih < p.otherLayers.length; ih++) {
p.otherLayers[ih].ref = $('#' + p.otherLayers[ih].id); // we will be referencing these a lot
$(p.otherLayers[ih].ref).width(Math.ceil(p.panelCount * p.panelWd * p.otherLayers[ih].ratio)); // This might be better to bake into style tags serverside
$('#' + p.otherLayers[ih].id + ' .p').width(Math.round(p.panelWd * p.otherLayers[ih].ratio)); // This might be better to bake into css
$('#' + p.otherLayers[ih].id + ' .p').text(Math.round(p.panelWd * p.otherLayers[ih].ratio)); // Helper line
p.parallax(p.otherLayers[ih].ref, p.otherLayers[ih].ratio);
}
};
this.parallax = function (containerRef, ratio) {
containerRef.css({ left: (1 - ratio) * (p.panelWd / 2 + $(window).scrollLeft()) + 'px' });
}
p.crunchWinVars();
p.init();
第一个方法处理与浏览器相关, 然后将舞台居中并将准确的宽度应用于溢出控件. 如果我们的视差只有一个单独面板的视差, 则溢出的宽度将只是屏幕宽度. 不会有滚动, 视差器实际上不会做任何事情. 我们需要将其增加每个附加静态面板的宽度.
第二个方法启动视差并设置不同层的面板宽度以及层本身的总宽度. 由于面板都是向左浮动的, 所以我们不能给它们包裹的机会. 它还会在首次调用视差函数.
最后, 视差, 我通过非常低效的反复试验方法得出了这个方程. 我将永远感激任何能找到正确的数学证明来证明这个方程为何有效的人, 所以, 请努力吧.
一旦我们将以下代码插入 setInterval
循环, 我们就会设置一个功能性视差器.
for (var ih = 0; ih < p.otherLayers.length; ih++) {
p.parallax(p.otherLayers[ih].ref, p.otherLayers[ih].ratio);
}
当我们滚动面板时, 确定何时展开叙述的函数是 aParallax 类之外的唯一函数. 我们将在文档的头部定义它.
function panelEventSniffer() {
var scroll = $(window).scrollLeft();
if (scroll >= -200 && scroll < 200 && p.panelHovered != 1) {
p.panelNarr('#c_1');
p.panelHovered = 1;
}
else if (scroll >= 200 && scroll < 600 && p.panelHovered != 2) {
p.panelNarr('#c_2');
p.panelHovered = 2;
}
else if (scroll >= 600 && scroll < 1000 && p.panelHovered != 3) {
p.panelNarr('#c_3');
p.panelHovered = 3;
}
else if (scroll >= 1000 && scroll < 1400 && p.panelHovered != 4) {
p.panelNarr('#c_4');
p.panelHovered = 4;
}
else if (scroll >= 1400 && scroll < 1800 && p.panelHovered != 5) {
p.panelNarr('#c_5');
p.panelHovered = 5;
}
else if (scroll >= 1800 && scroll < 2200 && p.panelHovered != 6) {
p.panelNarr('#c_6');
p.panelHovered = 6;
}
}
在 Hobo Lobo 上, 我生成了这个条件服务器端. 如果这完全用 JavaScript
实现, 更新起来会更麻烦, 而且用处也更少. 通过这种方式, 我们可以在每个面板的基础上运行任意代码, 而不是 JavaScript
的其他部分, 这些代码可以在自己的文件中, 并且只能下载一次. 条件从 -200px (浏览器窗口外静态面板不可能的一半)开始, 以保持其整洁并与所有后续值呈线性.
this.panelNarr = function (aPanel) {
var elevator = $('#panelControl a[href="' + aPanel + '"]');
var narrative = $(aPanel + ' .narrative');
var overflowNewHt = narrative.outerHeight() + 500;
$('.narrative:visible').stop(true, true).slideUp(100); // roll up text for other slides, if any are unfurled
if (!elevator.hasClass('clicked')) {
elevator.addClass('clicked'); // add clicked class to the panelControl for page that we just passed by
}
if (overflowNewHt > p.overflowControl.outerHeight()) {
p.overflowControl.animate({ 'height': overflowNewHt }, 200, 'easeInOutSine');
}
narrative.slideDown(500); // unfurl narrative
if (p.winWd != $(window).width()) {
p.crunchWinVars(); // sometimes extending the overflow adds the vertical scrollbar, so we need to account for that
}
};
this.panelControl = function () {
$('#panelControl a').click(function (elevator) {
elevator.preventDefault();
p.correctScroll($(this).attr('href'));
});
}
this.correctScroll = function (hash, duration) {
if (duration === undefined) {
var duration = 8345;
}
if ($(window).scrollTop()) {
// scrollTo will take forever scrolling up if we have x & y queued in a single line, so doing it separately allows us to have a brisker upscroll and a longer side one
$.scrollTo(hash, { 'axis': 'y', 'queue': true, 'duration': Math.floor($(window).scrollTop() * 3 / 2 + 200), 'offset': { 'top': -90 } });
}
$.scrollTo(hash, { 'axis': 'x', 'queue': true, 'duration': duration, 'offset': { 'left': -p.winHoriSp2Panel }, 'easing': 'easeInOutSine' });
}
p.panelControl();
其余方法是不言自明的. 第一个展开叙述并进行与展开相关的清理工作. 第二个将平滑动画 CorrectScroll
函数绑定到面板控件. 第三个动画滚动, 独立地执行两个轴, 以便 y 滚动快速发生, 而 x 滚动受益于平滑的缓动.
最后, 确保在 $(window).resize()
上执行 cruchWinVars()
并在 setInterval()
内执行 panelEventSniffer()
. 现在你就拥有了充满想象的简陋的视差器. 万岁! 走起!
还有一些可以改进. 事实上, Parallaxer 在移动端浏览器上的表现很不稳定. 我还没有探索劫持触摸事件和手势让 IOS 在正确的时间计算偏移量. 我可能可以尝试不使用 scrollTo 插件; 使用它是因为早期开发时用来节省时间. 另外, 应该有一种更优雅的方式让用户在制作动画时中断 CorrectScroll. 等等. 改进的空间是无限的.
有些东西我不想加进去, 比如加载屏幕, 我喜欢我们所拥有的木偶剧院的感觉. 如果你知道要寻找什么, 你可以看到弦乐、椽子上的人以及幕间移动家具的忍者. 留下一些粗糙感会带来更温暖的体验. 艺术家的草率之手总让人赏心悦目.
02. PNG 铅笔画的一种方法
现在开始使用 Photoshop! 我使用 CS3 是因为我有一台 PowerPC 并且已经被各地的开发者都抛弃了. 为你的系统调整下面的键盘快捷键.
使用包含每个视差图层的五个智能对象设置主文档, 并构建一个操作来生成拨至面板的图层组合, 例如 Parallaxer 的面板控件.
我在 4 英寸的纸条上绘制艺术作品, 并使用灯箱进行配准.
我以 400 dpi 的分辨率扫描所有内容并稍微清理下, 然后将其放入指定的智能对象中. 在绘画时, 我通常使用与我所画的任何东西形成鲜明对比的颜色, 而不是棋盘. 这是一张老鼠画, 在调整了一些关卡并避开了一些纸粒后, 尺寸和位置都已确定.
将焦点放在绘图图层上, 全选并复制到剪贴板, 然后取消选择并创建纯色图层.
Alt + 单击
进入颜色图层蒙版. 文件应该变成白色, 我们现在只能看到蒙版. 将绘图粘贴到蒙版中, 然后使用反斜杠()键退出视图. 单击选项 + i
反转蒙版. 这是我们看到的:
使用绘图板在其他单色图层上绘制填充和高光.
关闭狂热的火烈鸟图层, 切片, 保存为 24 位 PNG 并进行优化.
现在你就得到了它. 你现在是一名经过认证的视差者. 祝你好运!
Stevan Živadinović 曾经出生在一个虚构的国家, 但后来变得更好了. 他获得了认可机构的认证, 可以从事艺术创作. 在 The Nihilist Canary 和 Hobo Lobo of Hamelin 中看到他.