
在烤面包机上,他们经常问到如何制作房屋的交互式图,房屋内部结构的平面图,选择具有相关房屋信息的楼层或公寓的能力,当您将鼠标悬停在照片上时显示有关特定产品详细信息的能力等。 它不是关于三维模型,而是关于能够突出显示某些细节的图片。 所有这些任务都是相似的并且很简单地解决了,但是仍然存在问题,因此今天我们将看看如何使用SVG,图形编辑器和少量JavaScript完成这些任务。
SVG的选择是因为它是开发和调试的最简单选择。 我遇到了一些建议所有这些工作都在画布上完成的人,但是要了解正在发生的事情要困难得多,并且需要以某种方式提前计算曲线上所有点的坐标,但是在这里,我打开了开发人员的工具,立即看到了整个结构,所有对象,与之互动,其他一切都可以在人性化的界面中用鼠标单击。 普通2d画布和SVG之间的性能几乎没有差异。 WebGL在这方面可能会有所作为,但是开发时间表会大大增加,更不用说进一步的支持了,这并不总是适合预算。 我什至会说“不适合”。
在本教程中,我们将为某个虚拟站点做一些类似的小部件,以便在特定区域租用私人房屋,但是很明显,创建此类问题的原则适用于任何主题。
开始使用
我将以Inkscape为例展示所有内容,但是所有其他相同功能都可以在其他编辑器中使用相似的功能执行。
要工作,我们需要两个主对话框:
- XML编辑器(Ctrl + Shift + X或带有尖括号的图标)-以标记的形式查看文档的结构并编辑单个元素。
- 填充和描边(Ctrl + Shift + F或框架中带有画笔的图标)-主要用于填充轮廓。
立即启动它们并继续创建文档。
如果您不小心将它们拖到一个单独的窗口中,则可以单击此窗口的上框(没有任何内容),然后将它们拖回到主窗口中。 这不是完全直观的,而是很方便的。
打开一张可以看到该区域的照片。 我们可以选择将图片本身作为base64字符串或外部链接插入。 由于它很大,请选择链接。 然后,当实现网站页面中的所有内容时,我们将手动更改图片的路径。 将创建一个SVG文档,其中将通过image标签嵌入照片。
对于嵌入在SVG中,嵌入在HTML中的光栅图像,您可以使用延迟加载以及页面上的普通图像。 在此示例中,我们将不讨论此类优化,但在实际工作中不要忘记它们。
在当前阶段,我们看到的是这样的:

现在创建一个新图层(Ctrl + Shift + N或菜单>图层>添加图层)。 在XML编辑器中,我们看到规则g元素已经出现。 虽然我们还没做完,但是我们可以给它提供一个类,稍后将在脚本中使用该类。
不要依赖id。 界面越复杂,就越容易使它们重复并得到奇怪的错误。 在我们的任务中,他们仍然没有任何好处。 因此,类或数据属性是我们的选择。
如果仔细查看XML编辑器中的文档结构,您会发现其中有很多多余的东西。 任何或多或少复杂的矢量图形编辑器都会在文档中添加自己的内容。 要用手去除所有这些,是一项漫长而费力的工作,编辑器将不断添加一些内容。 因此,仅在工作结束时才从垃圾中清除SVG。 最好是自动形式,因为有现成的选项,例如相同的svgo 。
找到一个名为Draw Bezier曲线和直线(Shift + F6)的工具。 有了它,我们将在对象周围绘制闭合轮廓。 在我们的任务中,我们需要勾勒出所有建筑物的轮廓。 例如,我们将自己限制为六个,但是在实际条件下,应该预先分配时间以准确地概述所有必需的对象。 尽管经常发生许多相似的实体,但是建筑物上的相同楼层可以完全相同。 在这种情况下,您可以加快速度并复制粘贴曲线。
圈出必要的建筑物后,我们返回XML编辑器,添加类,或者最有可能的是,为它们添加带有索引的数据属性(可以使用地址,但是由于我们有一个虚拟区域,所以只有索引),然后将所有内容移至先前创建的图层,以便将所有内容“放置在货架上”。 顺便说一句,将图片移到那里也很有用,因此所有内容都集中在一个地方,但是这些都是小事。
现在,选择一条路径(建筑物周围的一条曲线)后,您可以使用Ctrl + A或“菜单”>“编辑”>“全选”将其全部选中,然后同时进行编辑。 您需要在“填充和描边”窗口中全部绘制它们,同时删除那里多余的描边。 好吧,或者如果出于设计原因需要,可以添加它。

即使在设计上没有必要,也可以使用所有颜色以最小的不透明度值绘制所有轮廓。 事实是,聪明的浏览器相信您不能单击空白路径,而只能单击淹没的路径-即使没有人看到,也可以单击。
在我们的示例中,我们将在白色处留一点高光,以更好地查看我们使用的建筑物,保存所有内容并平稳地移至浏览器和更熟悉的代码编辑器。
基本例子
让我们创建一个空的html页面,将生成的SVG直接粘贴到其中,并添加一些CSS,以使屏幕上没有任何内容。 没有什么可评论的。
.map { width: 90%; max-width: 1300px; margin: 2rem auto; border: 1rem solid #fff; border-radius: 1rem; box-shadow: 0 0 .5rem rgba(0, 0, 0, .3); } .map > svg { width: 100%; height: auto; border-radius: .5rem; }
我们记得,我们在建筑物中添加了类并使用它们,以便CSS或多或少地结构化。
.building { transition: opacity .3s ease-in-out; } .building:hover { cursor: pointer; opacity: .8 !important; } .building.-available { fill: #0f0 !important; } .building.-reserved { fill: #f00 !important; } .building.-service { fill: #fff !important; }
由于我们在Inkscape中定义了内联样式,因此我们需要在CSS中中断它们。 用CSS进行所有操作会更方便吗? 是的,没有。 这取决于情况。 有时别无选择。 例如,如果设计师将很多东西都绘制成彩色的,然后将所有东西都包裹在CSS中,然后将其膨胀到某种程度上变得不受欢迎。 在此示例中,我使用“不方便”选项来表明在解决问题的情况下它并不是特别令人恐惧。

假设我们收到了有关房屋的新数据,并根据房屋的当前状态为其添加了不同的类别:
const data = { id_0: { status: 'service' }, id_1: { status: 'available' }, id_2: { status: 'reserved' }, id_3: { status: 'available' }, id_4: { status: 'available' }, id_5: { status: 'reserved' }, messages: { 'available': ' ', 'reserved': '', 'service': ' 1-2 ' } }; const map = document.getElementById('my-map'); const buildings = map.querySelectorAll('.building'); for (building of buildings) { const id = building.getAttribute('data-building-id'); const status = data[`id_${id}`].status; building.classList.add(`-${status}`); }
我们得到这样的东西:

类似于我们需要的东西已经可见。 在此阶段,我们已经突出显示了地形上响应鼠标悬停的对象。 而且,通过标准addEventListener为他们添加对鼠标单击的响应并不难。
领导线
通常,需要进行绘制以将地图上的对象和页面上的某些元素与其他信息相连接的线条,以及将鼠标悬停在这些相同对象上时提供最少的工具提示的任务。 要解决这些问题, 引伸线微型库非常适合,它会为每种口味和颜色创建矢量箭头。
让我们为数据添加工具提示的价格并绘制这些线。
const data = { id_0: { price: '3000', status: 'service' }, id_1: { price: '3000', status: 'available' }, id_2: { price: '2000', status: 'reserved' }, id_3: { price: '5000', status: 'available' }, id_4: { price: '2500', status: 'available' }, id_5: { price: '2500', status: 'reserved' }, messages: { 'available': ' ', 'reserved': '', 'service': ' (1-2 )' } }; const map = document.getElementById('my-map'); const buildings = map.querySelectorAll('.building'); const info = map.querySelector('.info'); const lines = []; for (building of buildings) { const id = building.getAttribute('data-building-id'); const status = data[`id_${id}`].status; const price = data[`id_${id}`].price; building.classList.add(`-${status}`); const line = new LeaderLine( LeaderLine.pointAnchor(building, { x: '50%', y: '50%' }), LeaderLine.pointAnchor(info, { x: '50%', y: 0 }), { color: '#fff', startPlug: 'arrow1', endPlug: 'behind', endSocket: 'top' } ); lines.push(line); }
如您所见,没有复杂的事情发生。 该行具有到元素的“连接点”。 这些点相对于元素的坐标通常便于确定百分比。 通常,有很多不同的选项,列出并记住这些没有意义,因此我建议您只阅读文档。 我们需要这些选项之一(startLabel)来创建一个带有价格的小工具提示。
const line = new LeaderLine( LeaderLine.pointAnchor(building, { x: '50%', y: '50%' }), LeaderLine.pointAnchor(info, { x: '50%', y: 0 }), { startLabel: LeaderLine.captionLabel(`${price}/`, { fontFamily: 'Rubik Mono One', fontWeight: 400, offset: [-30, -50], outlineColor: '#555' }), color: '#fff', startPlug: 'arrow1', endPlug: 'behind', endSocket: 'top', hide: true } );
没有人愿意在图形编辑器中绘制所有技巧。 如果它们应该具有一致的内容,那么它甚至可以很方便。 尤其是如果希望向他们询问不同对象的不同位置。
我们还可以添加hide选项,以便所有行都不会显示为扫帚。 当您将鼠标悬停在它们对应的建筑物上时,我们将一次向他们展示一个:
building.addEventListener('mouseover', () => { line.show(); }); building.addEventListener('mouseout', () => { line.hide(); });
在这里您可以在信息处显示其他信息(在我们的情况下,仅是对象的当前状态)。 事实证明几乎需要什么:

这些东西很少是为移动设备设计的,但值得记住的是,它们通常在桌面上全屏显示,即使侧面有一些面板以提供更多信息,您也需要将所有内容进行精美的拉伸。 例如:
svg { width: 100%; height: 100%; }
此外,SVG元素的比例肯定与内部图像的比例不一致。 怎么办
比例不匹配
首先想到的是CSS的object-fit:cover属性。 但是有一点:它绝对不能与SVG一起使用。 即使可行,沿计划边缘的房屋也可能脱离计划的边缘,变得完全无法进入。 因此,在这里您需要采取一些更复杂的方法。
第一步。 SVG有一个prepareAspectRatio属性,该属性有点类似于object-fit属性(当然不是,但是...)。 通过为我们的计划的主要SVG元素preserveAspectRatio="xMinYMin slice"
,我们得到了一个扩展的电路,其边缘无空隙且无扭曲。
第二步 您需要使用鼠标进行拖放。 从技术上讲,我们仍然有这样的机会。 这里的任务比较复杂,特别是对于初学者。 从理论上讲,我们为鼠标和触摸屏提供了可以处理的标准事件,并获得了需要移动地图多少的值。 但实际上,您可能会在很长一段时间内陷入困境。 Hammer.js将会抢救 -另一个小型图书馆,它将整个内部厨房带到了自己的厨房,并提供了用于拖放,滑动等操作的简单界面。
我们需要在各个方向上移动包含建筑物和图片的图层。 轻松点:
const buildingsLayer = map.querySelector('.buildings_layer'); const hammertime = new Hammer(buildingsLayer); hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL });
默认情况下,hammer.js还包括对svaypas的识别,但是我们在地图上不需要它们,因此请立即将其关闭以免愚弄我们的头脑:
hammertime.get('swipe').set({ enable: false });
现在,您需要以某种方式了解将地图仅移到其边缘而不需要进一步移动时需要发布的内容。 通过在头部简单地表示两个矩形,我们了解到,为此,我们需要从所有四个侧面找出带有父元素(在本例中为SVG)的建筑物的层的压痕。 GetBoundingClientRect可以解决:
const layer = buildingsLayer.getBoundingClientRect(); const parent = svg.getBoundingClientRect(); const offsets = { top: layer.top - parent.top, bottom: layer.bottom - parent.bottom, right: layer.right - parent.right, left: layer.left - parent.left, };
为什么我们仍然没有文明(和稳定的工作)方式来做到这一点? 就性能而言,每次都拉getBoundingClientRect很不好,但是选择不是很丰富,并且几乎不可能注意到抑制作用,因此我们不会提出过早的优化以使一切正常。 一种或另一种方式,这使我们可以检查建筑物与图层的位置,并仅在有意义时移动所有内容:
let translateX = 0; let translateY = 0; hammertime.on('pan', (e) => { const layer = buildingsLayer.getBoundingClientRect(); const parent = svg.getBoundingClientRect(); const offsets = { top: layer.top - parent.top, bottom: layer.bottom - parent.bottom, right: layer.right - parent.right, left: layer.left - parent.left, }; const speedX = e.velocityX * 10; const speedY = e.velocityY * 10; if (speedX > 0 && offsets.left < 0) {
在边缘,通常值得放慢脚步,以免突然停止或猛拉。 因此,一切都来回像这样:
if (speedX < -offsets.left) { translateX += speedX; } else { translateX += -offsets.left * speedX / 10; }
有许多降低速度的选项。 这是最简单的。 是的,它不是很漂亮,但是却像软木塞一样无声无息。 在这种情况下,通常根据卡片的期望性能来选择系数。
如果您打开浏览器并在开发人员的工具中使用窗口大小进行播放,则可能会发现出现问题...
不纯力
在台式设备上,一切正常,但是在移动设备上,魔术发生了,即,body元素移动了而不是移动地图。 !! 仅演员那里是不够的。 尽管可以,但发生这种情况的原因是某处溢出某些内容,而某些覆盖没有设置为溢出:隐藏。 但是在我们的情况下,可能根本没有任何动作。
绿色排字机之谜:在svg元素内,div元素内,body元素内,html元素内有g元素。 Doctype自然是html。 如果添加transform:translation(...)来拖动g元素,则它将在笔记本电脑上按预期移动,甚至在手机上也不会移动。 控制台中没有错误。 但是肯定有一个错误。 浏览器是那里最后一个Chrome浏览器。 问题是为什么?
我建议您在没有答案的情况下思考大约10分钟。

答案哈哈 我欺骗了你 更确切地说,并非如此。 我描述了我们将通过手动测试观察的内容。 但实际上,一切都应按预期进行。 这不是错误,而是与touch-action的CSS属性相关的功能。 在我们的任务上下文中(突然之间!),它被发现并且具有一定的值,该值打破了与地图交互的整个逻辑。 因此,我们非常粗鲁地对待他:
svg { touch-action: none !important; }
但是回到羊群,看看结果(当然,最好在单独的选项卡中打开):
我决定不为任何流行的框架自定义代码,以使其保持中性的无格式空白的形式,您可以在创建组件时以此为基础。
结果如何?
花了很多时间,我们制定了一个计划,上面有一个位图图片,突出显示了它的各种细节,用箭头将未连接的对象和鼠标的反应连接起来。 我希望我能够传达“预算”版本中如何完成所有工作的基本思想。 正如我们在本文开头所提到的,存在许多不同的应用程序,包括与某种混淆的设计站点无关的应用程序(尽管在他们上经常使用这种方法)。 好吧,如果您正在寻找一些有关交互式的但已经是三维的东西,那么,我将保留指向该主题的文章的链接— 在Three.js上的三维产品展示最小 。