一、前情提要
其实我的本行是设计师嘛,所以总是经常需要浏览一下全景图什么的,但是网上的全景图网站总是有点限制,而且把图片全都交给在线服务总有种失控的感觉,所以这回我们来搞一个本地的全景图浏览器。
之前弄了一个 noob-service-vite
,用来通过 vite 伺服文件夹方便写点小功能。
思源笔记折腾记录 - 做一个白板 - 最最最基础的卡片显示 - 链滴 (ld246.com)
但是它只写死了伺服一个文件夹(也就是那个白板),为了方便鼓捣,我们来尝试弄一个新活。
二、准备工作
1、自动伺服新挂件
我们对之前的 noob-service-vite 做一点改造。
首先是让他能够自动识别 data/viteWidgets
中的文件夹:
noob-service-vite\util\file.js
let fs = require('fs')
let path = require('path')
export let vite挂件目录 = window.siyuan.config.system.workspaceDir +'/data/viteWidgets'
export function 读取挂件列表(){
let 挂件列表 =[]
let 路径列表= require('fs').readdirSync(vite挂件目录)
路径列表.forEach(
路径名=>{
//如果目录下有vite.config.js
if(fs.existsSync(path.join(vite挂件目录,路径名,'vite.config.js'))){
挂件列表.push({
name:路径名,
path:path.join(vite挂件目录,路径名)
})
}
}
)
return 挂件列表
}
2、自动伺服
这样之后,就可以改造最开始的 noob-service-vite
了
noob-service-vite\index.js
import noobApi from '../noobApi/index.js'
import { 读取挂件列表 } from './util/file.js'
import { 获取可用端口号 } from './util/port.js'
if (window.require) {
const path = require('path')
const vite = require('vite')
let vite服务注册表 = {}
读取挂件列表().forEach(
挂件属性 => {
创建vite服务(挂件属性.path)
}
)
function 创建vite服务(文件夹路径) {
return new Promise(async (resolve, reject) => {
let vite配置文件路径 = path.join(文件夹路径, 'vite.config.js')
if (!vite服务注册表.文件夹路径) {
let server = await vite.createServer({
configFile: vite配置文件路径,
root: 文件夹路径,
})
!vite服务注册表[文件夹路径]?vite服务注册表[文件夹路径]={}:null
vite服务注册表[文件夹路径]['server'] = server
let port
if (server.config.server.port) {
port = await 获取可用端口号(server.config.server.port)
} else {
port =await 获取可用端口号(6806)
}
await server.listen(port,'127.0.0.1').then(
s => {
console.log(文件夹路径,'的开发服务在:',port,'上启用')
vite服务注册表[文件夹路径]['port'] = port
}
)
resolve(server)
}
})
}
}
获取可用端口号的实现其实就是尝试能不能监听这个端口,如果不能就试个新的:
export async function 获取可用端口号(端口号){
return new Promise((resolve, reject) => {
let http = require('http')
let 测试服务 = http.createServer()
let 可用端口号 = 端口号||3000
测试服务.on(
'listening',()=>{
console.log(端口号)
测试服务.close(()=>{
resolve(可用端口号)
})
}
)
测试服务.on(
'error',async(error)=>{
console.log(error)
if(error.code==='EADDRINUSE'){
resolve(await 获取可用端口号(可用端口号+1))
}
else{
reject(error)
}
}
)
测试服务.listen(端口号)
})
}
二、实现简单的全景图浏览
1、安装 photo sphere viewer
这个很简单啦:
npm i photo sphere viewer --registry=https://registry.npmmirror.com
2、最简单的球面全景图查看器
欸,我就是不写 index.html
的内容,你们试着自己弄一下。
要实现一个最简单的全景图 i 浏览只需要几行代码啦:
import { Viewer } from 'photo-sphere-viewer';
//这里是引入了css,vite里面你这么干跟在html里面引入差不多
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'
const viewer = new Viewer({
container: document.querySelector('#viewer'),
//这个是我之前上传到工作空间里的一张全景图
panorama: 'assets/11111D5_全景图 1_20221211_061304-20221211182953-7i0dt0q.jpg',
});
就像这样:
这张全景的原图长这样:
第一个全景图就渲染出来了~~
3、 加上画廊
photo sphere viewer 有很多插件,其中有一个 PhotoSphereViewer.GalleryPlugin
是用来生成一连串的全景图的。
PhotoSphereViewer.GalleryPlugin
这里我们约定文件名为:全景图-< 项目名 >-< 空间名 >-.jpg 的附件文件就是某一个系列的全景图。
所以这里可以搞一个 sql:
select *
from assets
where name like '全景图-${项目名}-%'
然后我们来实验一下:
sphereViewer\src\index.js
import { Viewer } from 'photo-sphere-viewer';
import {GalleryPlugin} from 'photo-sphere-viewer/dist/plugins/gallery.js'
import 核心api from 'http://127.0.0.1:6806/snippets/noobApi/util/kernelApi.js'
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'
import 'photo-sphere-viewer/dist/plugins/gallery.css'
import { 获取地址参数 } from '../../whiteBoard/src/data';
let {project} =获取地址参数()
console.log(project)
let sql =`select * from assets where name like '全景图-${project}-%'`
let 全景图列表 = await 核心api.sql({stmt:sql})
console.log(全景图列表)
全景图列表.forEach(
item=>{
item.panorama=item.path
item.thumbnail =item.path
let 空间名 = item.name.split('-')[2]
item.options ={
caption: 空间名,
}
}
)
console.log(Viewer,GalleryPlugin)
const viewer = new Viewer({
container: document.querySelector('#viewer'),
panorama: 全景图列表[0].path,
plugins: [
[GalleryPlugin, {
visibleOnLoad: true,
}],
],
});
const gallery = viewer.getPlugin(GalleryPlugin);
gallery.setItems(全景图列表)
嗯,好像渲染成功了~~~。
4、更方便的项目创建
啊,我们现在有一个全景图浏览器了,但是一张张图上传到思源的 assets 里面好像太麻烦了点,有没有更方便的办法咧?
首先来设想一个简单的全景图项目文件夹结构
-
项目名称
-
空间名 1
- 全景图们
- 其他数据
-
空间名 2
-
空间名 3
-
全部通过 assets
来管理好像有点麻烦,而且它的嵌套结构也不适合放到 assets 里面一起管理,全景图的数据也不大可能会被用到别的地方。
所以,我们来起一个后端吧,专门用来给这个网页提供服务(什么叫 BFF 啊~~)。
之前在 noob-service-vite 中,我们直接让 vite 来创建和监听服务器,这回改一下,使用中间件模式。
function 创建vite服务(文件夹路径) {
return new Promise(async (resolve, reject) => {
let vite配置文件路径 = path.join(文件夹路径, 'vite.config.js')
if (!vite服务注册表.文件夹路径) {
let vite中间件 = await vite.createServer({
configFile: vite配置文件路径,
root: 文件夹路径,
server: {
middlewareMode: "html"
}
})
const app = express()
!vite服务注册表[文件夹路径] ? vite服务注册表[文件夹路径] = {} : null
vite服务注册表[文件夹路径]['server'] = app
let port = vite中间件.config.server.port || 6807
port = await 获取可用端口号(port)
let server = await app.listen(port, '127.0.0.1', async () => {
vite服务注册表[文件夹路径]['port'] = port
vite服务注册表[文件夹路径]['server'] = server
vite服务注册表[文件夹路径]['app'] = app
vite服务注册表[文件夹路径]['vite'] = vite中间件
console.log(文件夹路径, '的开发服务在:', 'http://127.0.0.1:' + port, '上启用')
if (require('fs').existsSync(文件夹路径 + '/backend/index.js')) {
//因为某些蛋疼的原因,这里只能用require
let router = require(文件夹路径 +'/backend/index.js')
//主意router不要妨碍vite的工作,也就是别占了vite的坑
app.use(router)
app.use(vite中间件.middlewares)
}
})/*.then(
s => {
let {address,port} = server.httpServer.address()
console.log(文件夹路径,'的开发服务在:','http://'+address+':'+port,'上启用')
vite服务注册表[文件夹路径]['port'] = port
console.log(server)
}
)*/
resolve(server)
}
})
}
这样之后,随便试一下,就把文件存在 data/widgetsData
里面吧。
按照上面约定的后端文件入口,先弄两个接口吧:
const express = require('express')
let router = express.Router()
const fs = require('fs')
const path = require('path')
let 数据路径 = path.join(window.siyuan.config.system.workspaceDir,'data','widgetsData','sphereViewer')
if(fs.existsSync(数据路径)){
//为了避免跟思源的接口重合,挂件后端的接口全都加上widget前缀好了
router.use('/widgetData',express.static(数据路径))
router.use('/widgetApi/projects/getAll',(req,res)=>{
res.json(fs.readdirSync(数据路径)
})
}
//上面已经做了自动导入,回自动把这个接口混入vite的开发服务器当中
module.exports=router
这样之后,往这个文件夹里面放一点文件:
这个时候访问一下 127.0.0.1:6809/widgetApi/projects/listAllProjects
(这个端口是在 vite.config.js 里设置的)。
可以看到我们刚刚写的后端已经返回了项目列表。
我们之前是从思源的 sql 获取数据的,所以上面的查看器的获取方式也要改一下。
但是因为之后可能还需要其他来源的全景图数据的渲染,所以这里也使用一个跟之前做简单白板的时候类似的适配器。
sphereViewer\src\adapters\sql.js
import { 核心api } from "../../../whiteBoard/src/data"
export class sql全景图适配器{
获取项目列表(){
}
async 获取全景图列表(项目名){
let sql =`select * from assets where name like '全景图-${项目名}-%'`
let 全景图列表 = await 核心api.sql({stmt:sql})
全景图列表.forEach(
item=>{
item.panorama=item.path
item.thumbnail =item.path
let 空间名 = item.name.split('-')[2]
item.options ={
caption: 空间名,
}
}
)
return 全景图列表
}
}
import { Viewer } from 'photo-sphere-viewer';
import {GalleryPlugin} from 'photo-sphere-viewer/dist/plugins/gallery.js'
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'
import 'photo-sphere-viewer/dist/plugins/gallery.css'
import { 获取地址参数 } from '../../whiteBoard/src/data';
import {sql全景图适配器} from "./adapters/sql.js"
let {project} =获取地址参数()
console.log(project)
let 当前适配器 = new sql全景图适配器()
let 全景图列表= await 当前适配器.获取全景图列表(project)
const viewer = new Viewer({
container: document.querySelector('#viewer'),
panorama: 全景图列表[0].path,
plugins: [
[GalleryPlugin, {
visibleOnLoad: true,
}],
],
});
const gallery = viewer.getPlugin(GalleryPlugin);
gallery.setItems(全景图列表)
这个时候访问 127.0.0.1:6809/?project=云 park,应该可以看到跟之前一样的结果。
然后来试一下写一个后端适配器:
export class sql全景图适配器{
获取项目列表(){
}
async 获取全景图列表(项目名){
let res = await fetch('widgetApi/projects/getScenesByName',{
method:"POST",
body:{
name:项目名
}
})
let 全景图列表 = await res.json()
return 全景图列表
}
}
啊,是不是跟从思源获取数据挺像的,这个时候 getScenesByName
这个接口还不在,所以我们需要实现一下它。
router.post('widgetApi/projects/getScenesByName',(req,res)=>{
let {name} = req.body
let 场景列表 = []
let 项目文件列表 = fs.readdirSync(path.join(数据路径,name))
项目文件列表.forEach(
路径名=>{
let 缩略图路径
if( fs.existsSync(path.join(数据路径,name,路径名,'thumbnail.jpg'))){
缩略图路径 = path.join('/widgetData',name,路径名,'thumbnail.jpg').replace(/\\/g,'/')
}else if( fs.existsSync(path.join(数据路径,name,路径名,'_f.jpg')) ){
缩略图路径=path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/')
}else if( fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg')) ){
缩略图路径=path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/')
}
if(fs.existsSync(path.join(数据路径,name,路径名,'_b.jpg'))){
场景列表.push(
{
id:路径名,
panorama:{
left: path.join('/widgetData',name,路径名,'_r.jpg').replace(/\\/g,'/'),
front: path.join('/widgetData',name,路径名,'_b.jpg').replace(/\\/g,'/'),
right: path.join('/widgetData',name,路径名,'_l.jpg').replace(/\\/g,'/'),
back: path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/'),
top: path.join('/widgetData',name,路径名,'_u.jpg').replace(/\\/g,'/'),
bottom: path.join('/widgetData',name,路径名,'_d.jpg').replace(/\\/g,'/'),
},
thumbnail:缩略图路径,
options:{
caption:name,
}
}
)
}
else if(fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg'))){
场景列表.push(
{
id:路径名,
panorama:path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/'),
options:{
caption:name,
},
thumbnail:缩略图路径,
}
)
}
}
)
res.json(场景列表)
})
看起来应该跟上面的这个差不多对吧,就是识别文件夹里的文件,然后匹配下路径然后返回。
不过这里有个地方需要注意:
panorama:{
//这里的文件名是按照酷家乐的下载包内部名称弄的
//但是除了上下不变以外,需要把左边映射到右边,右边弄到左边,其他的依此类推
left: path.join('/widgetData',name,路径名,'_r.jpg').replace(/\\/g,'/'),
front: path.join('/widgetData',name,路径名,'_b.jpg').replace(/\\/g,'/'),
right: path.join('/widgetData',name,路径名,'_l.jpg').replace(/\\/g,'/'),
back: path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/'),
top: path.join('/widgetData',name,路径名,'_u.jpg').replace(/\\/g,'/'),
bottom: path.join('/widgetData',name,路径名,'_d.jpg').replace(/\\/g,'/'),
},
因为渲染器给出的六面全景图一般是按照“人在方盒子里面”来适配六个面的,而 PhotoSphereViewer
是按照“人在方盒子外面”来读取的,所以读取文件的时候,除了上下不用反之外,其他前后左右都要反一下~~~
我们试着使用一下这个适配器:
import {后端适配器} from './adapters/internal.js'
let {project} =获取地址参数()
console.log(project)
let 当前适配器 = new 后端适配器()
let 全景图列表= await 当前适配器.获取全景图列表(project)
const viewer = new Viewer({
container: document.querySelector('#viewer'),
panorama: 全景图列表[0].path,
plugins: [
[GalleryPlugin, {
visibleOnLoad: true,
}],
],
});
const gallery = viewer.getPlugin(GalleryPlugin);
gallery.setItems(全景图列表)
然后它就报错了:
因为我们使用的 express
后端并没有自带解析 post
请求的 body
的能力,这个时候需要一个 body-parser
一样的,首先需要安装它:
npm i --registry=https://registry.npmmirror.com body-parser
然后引入并使用:
const bodyParser = require('body-parser')
router.post('/widgetApi/projects/getScenesByName',
(req,res,next)=>{
//强制按json解析,这样前端就不用设置content-type了
req.headers['content-type']='application/json'
//记得调用next()
next()
},
bodyParser.json(),
let {name} = req.body
let 场景列表 = []
let 项目文件列表 = fs.readdirSync(path.join(数据路径,name))
项目文件列表.forEach(
路径名=>{
let 缩略图路径
if( fs.existsSync(path.join(数据路径,name,路径名,'thumbnail.jpg'))){
缩略图路径 = path.join('/widgetData',name,路径名,'thumbnail.jpg').replace(/\\/g,'/')
}else if( fs.existsSync(path.join(数据路径,name,路径名,'_f.jpg')) ){
缩略图路径=path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/')
}else if( fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg')) ){
缩略图路径=path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/')
}
if(fs.existsSync(path.join(数据路径,name,路径名,'_b.jpg'))){
场景列表.push(
{
id:路径名,
panorama:{
left: path.join('/widgetData',name,路径名,'_r.jpg').replace(/\\/g,'/'),
front: path.join('/widgetData',name,路径名,'_b.jpg').replace(/\\/g,'/'),
right: path.join('/widgetData',name,路径名,'_l.jpg').replace(/\\/g,'/'),
back: path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/'),
top: path.join('/widgetData',name,路径名,'_u.jpg').replace(/\\/g,'/'),
bottom: path.join('/widgetData',name,路径名,'_d.jpg').replace(/\\/g,'/'),
},
thumbnail:缩略图路径,
options:{
caption:name,
}
}
)
}
else if(fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg'))){
场景列表.push(
{
id:路径名,
panorama:path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/'),
options:{
caption:name,
},
thumbnail:缩略图路径,
}
)
}
}
)
res.json(场景列表)
}
然后再看一下 127.0.0.1:6809/?project=云 park48
这个时候它显示:
这是因为 PhotoSphereViewer
默认使用的是球面图适配器,而我们优先返回的是六面图,有关它的适配器可以参考文档:
Adapters | Photo Sphere Viewer (photo-sphere-viewer.js.org)
先不纠结,按照文档使用六面图适配器看看:
sphereViewer\src\index.js
let 全景图选项 = {
container: document.querySelector('#viewer'),
panorama: 全景图列表[0].panorama,
plugins: [
[GalleryPlugin, {
visibleOnLoad: true,
}],
],
}
//如果有六面图文件,就使用六面图适配器
if(全景图列表[0].panorama.left){
全景图选项.adapter = CubemapAdapter
}
let viewer= new Viewer(全景图选项)
这回显示对了,效果就像这样:
这回好像终于干了点跟设计师沾边的活儿了~~~
目前的代码片段的地址位于:
leolee9086/snippets (github.com)
viteWidgets 的地址位于
leolee9086/viteWidgets (github.com)
啊 全景图文件我就不提供了,自己试试吧。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于