
声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
专栏上篇文章《Three.js 进阶之旅:全景漫游-初阶移动相机版》中通过创建多个球体全景场景并移动相机和控制器的方式实现了多个场景之间的穿梭漫游。这种方式的缺点也是显而易见的,随着全景场景的增加来创建对应数量的球体,使得空间关系计算难度提升,并且大幅降低浏览器渲染行性能。在上一篇文章的基础上,本文通过以下几点对全景功能加以优化,最后实现一个可以应用到实际项目中的在线看房案例。通过阅读本文和实践案例,你将学到的知识包括:使用 Three.js 用新的技术思路实现多个场景的加载和场景间的丝滑过渡切换、随着空间一直和角度实时变化的房源小地图、在全景场景中添加如地面指引、空间物体展示、房间标注等多种类型的交互热点等。
我们先来看看本文在线看房案例的最终实现效果,页面主体由代表多个房间的全景图 ? 、全景空间中的用于标识物体的交互热点 ? 、显示房间名称的空间热点 ?️ 、用于地面前进指引的交互热点 ? 、固定在侧边的房间切换按钮 ◻️ 以及右上侧的房间小地图 ? 构成。左右拖动页面可以进行当前房间的全景预览,同时小地图上的锚点旋转角度和位置也根据当前房间的位置和旋转角度的变化而变化,使用鼠标滚轮或触摸板放大缩小页面可以查看房间全景图的整体和局部细节。

点击地面前进指引标记热点或空间中的房间名标签热点、以及固定在右边的房间名按钮时可以丝滑切换到对应的房间,固定在右侧的按钮和空间中的房间名标签之间相互联动,当页面视区无法看到空间中的房间名标签时,它会自动固定到右侧按钮处。点击房间中的物体标识热点可以与之产生交互。

打开以下链接,在线预览效果,gif 造成丢帧和画质损失,在线大屏访问效果更佳。
本专栏系列代码托管在 Github 仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
看完本文在线看房案例,我们先来总结下本文在上篇文章示例的基础上,做了哪些优化?下图几个标注点对应本文实现的一些新的功能和优化,通过以下几点的实现可以提升多个全景场景漫游项目的加载渲染性能和用户体验。

上篇文章示例我们通过创建多个球体移动相机和控制器的方式实现多个全景场景之间的漫游,而这篇文章中,我们将通过创建多个场景的方式,来实现两个全景场景之间的过渡漫游效果。
实现原理示意图如下所示,页面总共将创建 3 个场景,origin 表示当前场景,destination 表示目标场景,利用当前场景和目标场景合成用于展示过渡效果的 transition 过渡场景,当点击切换房间按钮时,三个场景的加载顺便分别为 origin -> transition -> destiontion,由此在视觉上形成从上个房间切换到下个房间并且伴随渐变过渡的场景漫游效果。

对应上面几个优化点及多个场景的切换原理,我们现在来一步步实现本文中的案例:
<canvas class="webgl"></canvas>
我们先来看看页面使用了哪些资源,OrbitControls 是镜头轨道控制器,可以在全景图场景中使用鼠标进行旋转和放大缩小;TWEEN 和 Animations 用于实现一些镜头补间动画效果;rooms, markers, roomLabels 是自定义的数据,分别表示房间信息、房间地面前进指引标记物和房间名称热点标记物;fragment 和 vertex 是用于实现原始场景和目标场景切换的动画过渡效果;TinyMap 是和当前房间位置和镜头旋转方向同步的小地图组件。
import * as THREE from 'three';
import { OrbitControls } from '@/utils/OrbitControls.js';
import { TWEEN } from 'three/examples/jsm/libs/tween.module.min.js';
import { rooms, markers, roomLabels } from '@/views/home/data';
import fragment from '@/shaders/cross/fragment.js';
import vertex from '@/shaders/cross/vertex.js';
import Animations from '@/utils/animations';
import TinyMap from '@/components/TinyMap.vue';
初始化 Three.js 构建三维场景的渲染器、场景、相机、控制器等基本元素,需要注意的是这次把场景命名为原场景 sceneOrigin,因为后续还要场景多个场景,通过多个场景间的动画过渡效果,实现各个房间全景图之间的穿梭漫游效果。
// 定义页面尺寸
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 原场景
const sceneOrigin = new THREE.Scene();
// 初始化相机
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.001, 1000);
camera.position.set(0, 16, 16);
// 镜头鼠标控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 页面缩放事件监听
window.addEventListener("resize", () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// 更新渲染
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 更新相机
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
});
// 页面重绘动画效果
const tick = () => {
controls && controls.update();
TWEEN && TWEEN.update();
renderer.render(sceneOrigin, camera);
requestAnimationFrame(tick);
};
现在来看看具体代码实现:创建原场景,并添加一个球体 ? 作为当前房间的全景图;创建目标场景,并添加一个球体 ? 作为目标场景的全景图;创建过渡场景,并添加一个平面几何体 ⬜ 到其中,该平面将用于展示原场景和目标场景之间的过渡效果,几何体采用 ShaderMaterial 着色器材质,并具有 progress、sceneOrigin、sceneDestination 三个由 JavaScript 传递到着色器的统一变量 uniforms。同时并创建了一个用于显示过渡场景的 OrthographicCamera 正交相机。
// 创建原场景
const sceneOrigin = new THREE.Scene();
const sphereGeometry = new THREE.SphereGeometry(16, 128, 128);
sphereGeometry.scale(1, 1, -1);
const originMesh = new THREE.Mesh(sphereGeometry, new THREE.MeshBasicMaterial({
map: mapOrigin,
side: THREE.DoubleSide,
}));
originMesh.rotation.y = Math.PI / 2;
sceneOrigin.add(originMesh);
// 创建目标场景
const sceneDestination = new THREE.Scene();
const destinationMesh = new THREE.Mesh(sphereGeometry, new THREE.MeshBasicMaterial({
map: mapDestination,
side: THREE.DoubleSide,
}));
destinationMesh.rotation.y = Math.PI / 2;
sceneDestination.add(destinationMesh);
data.sceneDestination = sceneDestination;
const textureDestination = new THREE.WebGLRenderTarget(sizes.width, sizes.height, {
format: THREE.RGBAFormat,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
});
// 创建过渡场景
const sceneTransition = new THREE.Scene();
const finalMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.ShaderMaterial({
extensions: {
derivatives: "#extension GL_OES_standard_derivatives : enable",
},
side: THREE.DoubleSide,
uniforms: {
progress: { value: 0 },
sceneOrigin: { value: null },
sceneDestination: { value: null },
},
vertexShader: vertex,
fragmentShader: fragment,
}));
sceneTransition.add(finalMesh);
// 创建过渡正交相机
let frustumSize = 1;
const cameraTransition = new THREE.OrthographicCamera(frustumSize / -2, frustumSize / 2, frustumSize / 2, frustumSize / -2, -1000, 1000);
然后在页面重绘方法中依次渲染它们,并更新过渡场景中平面几何体的材质变量:
// 页面重绘动画效果
const tick = () => {
// ...
renderer.setRenderTarget(textureDestination);
renderer.render(sceneDestination, camera);
renderer.setRenderTarget(textureOrigin);
renderer.render(sceneOrigin, camera);
finalMesh.material.uniforms.sceneDestination.value = textureDestination.texture;
finalMesh.material.uniforms.sceneOrigin.value = textureOrigin.texture;
finalMesh.material.uniforms.progress.value = data.progress;
renderer.render(sceneTransition, cameraTransition);
// ...
};

创建完场景后,我们使用着色器来实现过渡场景的动画效果,在顶点着色器中我们添加如下的变量和方法:
uniform float time;
varying vec2 vUv;
varying vec3 vPosition;
uniform vec2 pixels;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
在片段着色器中,我们使用从 JavaScript 中传递过来的变量 sceneOrigin 原场景、sceneDestination 目标场景以及过渡进度值 progress 作为参数,实现两个场景之间渐隐渐显效果的过渡动画 ✨ :
uniform float time;
uniform float progress;
uniform sampler2D sceneDestination;
uniform sampler2D sceneOrigin;
uniform vec4 resolution;
varying vec2 vUv;
varying vec3 vPosition;
void main(){
float progress1 = smoothstep(0.10, 1.0, progress);
vec4 sPlanet = texture2D(sceneOrigin, vUv);
vec4 s360 = texture2D(sceneDestination, vUv);
float mixer = progress1;
gl_FragColor = s360;
vec4 finalTexture = mix(sPlanet, s360, mixer);
gl_FragColor = finalTexture;
}

我们甚至可以再添加一个 distort 方法,实现华丽的穿梭效果:
vec2 distort(vec2 olduv, float pr, float expo) {
vec2 p0 = 2.*olduv - 1.;
vec2 p1 = p0/(1. - pr*length(p0)*expo);
return (p1 + 1.) * 0.5;
}
void main(){
float progress1 = smoothstep(0.10, 1.0, progress);
vec2 uv1 = distort(vUv, -90.*progress, progress*4.);
vec4 sPlanet = texture2D(sceneOrigin, uv1);
vec4 s360 = texture2D(sceneDestination, vUv);
float mixer = progress1;
gl_FragColor = s360;
vec4 finalTexture = mix(sPlanet, s360, mixer);
gl_FragColor = finalTexture;
}

此时我们就可以使用一个数组向下面这样批量定义好所有房间的路径,如从走廊可以通向卧室、客厅,用 currentRoom 和 destinationRoom 字段标识当前房间和目标房间,当触发切换房间动作时,我们用这两个字段拿到对应的全景贴图,然后更新原始场景 ? 和 目标场景 ? 中球体的材质属性 map 即可实现房间切换了。
const routes = [
// 从走廊向卧室
{
currentRoom: 'hall',
destinationRoom: 'bed-room',
},
// 从走廊向客厅
{
currentRoom: 'hall',
destinationRoom: 'living-room',
},
// ...
];
?关于着色器入门,可以看看此专栏另一篇文章《Three.js 进阶之旅:Shader着色器入门》
室内的交互热点,即在三维空间中添加的平面交互点,可以通过 Sprite、Canvas 等实现,但是它们的缺点是样式不容易改,本文中所有的交互热点都是使用页面的 DOM 节点实现的,因此直接使用 CSS 就能自由修改样式。
我们可以在房间数据中向下面这样配置标注室内物体的交互点信息和 Vector3 类型的位置信息,然后使用数组在页面上批量创建 DOM 节点:
{
name: '客厅',
interactivePoints: [
{
key: 'tv',
value: '电视',
cover: new URL('@/assets/images/home/cover_living_room_tv.png', import.meta.url).href,
position: new Vector3(-8, 2, -15),
},
{
key: 'art',
value: '艺术品',
cover: new URL('@/assets/images/home/cover_living_room_art.png', import.meta.url).href,
position: new Vector3(10.5, 0, -15),
},
// ...
在页面中这样渲染它们:
<div
v-for="(point, index) in interactivePoints"
:key="index"
:class="[`point-${index}`, `point-${point.key}`]"
@click="handleReactivePointClick(point)"
v-show="point.room === data.currentRoom"
>
<p class="p1">{{ point.value }}</p>
</div>
为了实现丝滑的扩散效果动画,室内物体标记热点使用了一张雪碧图作为动画帧,可以像下面这样播放雪碧图动画帧:
.point
background-image url('@/assets/images/sprites/interactive.png')
background-repeat: no-repeat
background-position: 0 0
background-size: 100%
background-position-y: 0
animation: interactivePointAnimation 2s steps(24) forwards infinite
animation-fill-mode both;
@keyframes interactivePointAnimation
0%
background-position: 0 0
to
background-position: 0 -1536PX;
为了交互热点能够显示在按配置好的位置属性 position 正确显示在页面上,我们还需要在页面重回方法 tick() 中实时更新它们的位置,并利用 Raycaster 检测标记点是否被遮挡来显示或隐藏交互点。
const tick = () => {
// 产品介绍标记物显隐
for (const point of _points) {
// 获取2D屏幕位置
const screenPosition = point.position.clone();
const pos = screenPosition.project(camera);
raycaster.setFromCamera(screenPosition, camera);
const intersects = raycaster.intersectObjects(sceneTransition.children, true);
if (intersects.length === 0) {
// 未找到相交点,显示
point.element.classList.add("visible");
} else {
// 获取相交点的距离和点的距离
const intersectionDistance = intersects[0].distance;
const pointDistance = point.position.distanceTo(camera.position);
// 相交点距离比点距离近,隐藏;相交点距离比点距离远,显示
intersectionDistance < pointDistance
? point.element.classList.remove("visible")
: point.element.classList.add("visible");
}
// 物体转动到背面时隐藏,否则显示
pos.z > 1
? point.element.classList.remove("visible")
: point.element.classList.add("visible");
}
// ...
};

?本文中其他交互点如房间标签名标记点、地面前进指引标记点等与室内物体标注交互点的实现原理都是一样的,后续将不再赘述。
固定在侧边的按钮用保存房间信息的数组 rooms 即可生成,我们可以使用 v-show 或 v-if 对房间标签进行过滤,当我们的全景图处于当前房间时,就可以不显示当前房间的标签名。如下图中,当前位置是走廊的全景空间,切换按钮处就仅显示走廊以外的其他房间的切换按钮。
<div class="switch">
<span
class="button"
v-for="(room, index) in rooms"
:key="index"
@click="handleSwitchButtonClick(room.key)"
v-show="room.key !== data.currentRoom"
>
</span>
</div>

为了在三维全景空间中漫游时清晰地知道各个房间在当前房间的哪个位置,我们可以向实现室内物体交互热点一样,在三维空间中添加房间标签名标记点。比如当我们位于客厅时,从客厅可以看到厨房和走廊,此时我们就可以向下面这样在客厅空间中配置厨房和走廊名称的房间名称标记点,然后像添加室内物体标记热点地原理一样,在空间中创建对应的 html 标签并在 tick() 方法中更新每个标签的显示隐藏属性即可。
// ...
{
key: 'living-room',
name: '客厅',
visible: true,
visibleRooms: [
{
key: 'hall',
position: new Vector3(-12, 2, 15),
},
{
key: 'kitchen',
position: new Vector3(-12, 2, -15),
}
],
},

当我们完成上两个步骤时,以当前房间位于客厅为例,此时页面上就会显示两个一样的房间标签名:侧边固定按钮处有走廊,随空间转动的浮动房间标签热点中也有走廊,同时出现两个相同的名称就会使页面变得混乱,让使用的地人产生疑惑。
我们可以像这样优化以下这个功能:当位于空间中的房间名称标签在可见时,就从侧边固定按钮标签中将其移除;当位于空间中的房间名称标签转动到后方不可见时,侧边固定按钮处就显示该房间名称。代码中我们可以在 tick() 方法中像下面这样实现,初始化时我们对 rooms 数组的每一项设置一个属性 visible: true,当在页面重绘动画中检测到某房间名称标记点转出屏幕时,就将该标签代表的房间的 visible 属性设置为 false,当该标签跟随房间转动再次出现在屏幕上时,就将 visible 修改为 true。
tick () {
data.filtederRooms.forEach((item) => item.visible = true);
for (const label of _roomLabels) {
// ...
// 标记物旋转出屏幕时显示侧边贴靠导航,标记物出现在屏幕时隐藏侧边贴靠导航
if (Math.abs(pos.x) < 1.2 && pos.z < 1) {
data.filtederRooms.forEach((item) => {
if (item.key === label.key) {
item.visible = false;
}
});
} else {
data.filtederRooms.forEach((item) => {
if (item.key === label.key) {
item.visible = true;
}
});
}
}
}
然后,我们再把修改下生成固定在侧边的房间按钮的方法,用过滤过的房间数组数组 filtederRooms 代替之前的 rooms,并判断该标签不是当前所处的的房间且该房间的可见性为 true 时才显示该侧边固定按钮。
<div class="switch">
<span
class="button"
v-for="(room, index) in data.filtederRooms"
:key="index"
@click="handleSwitchButtonClick(room.key)"
v-show="room.key !== data.currentRoom && room.visible === true"
>
</span>
</div>
此时,视觉上就会形成当我们转动房间全景场景时,如果位于空间的房间名称标签转出到屏幕之外时,就会自动固定到侧边的视觉效果 ? 。

在地面上添加前进指引热点,可以引导用户从当前空间漫游穿梭到其他空间,利用上面步骤①中定义好的数组 routes,我们在其中添加一个地面热点在三维空间中的 Vector3 类型位置点信息,然后和前面创建室内物体标注交互点一样,批量创建即可。
const routes = [
// 从走廊向卧室
{
currentRoom: 'hall',
destinationRoom: 'bed-room',
position: new Vector3(0, 0, 0),
},
// 从走廊向客厅
{
currentRoom: 'hall',
destinationRoom: 'living-room',
position: new Vector3(1, 1, 1),
},
// ...
];

在线看房 ? 页面最重要的特征就是有户型小地图 ?,用户可以清晰地知道该房源的详细结构以及当前所处的房间位置,当随着房间全景图在三维空间中旋转,位于小地图上的锚点 ⬆️ 也对应旋转和移动。

我们来看看具体是如何实现的:我们先创建一个 TinyMap.vue 组件,其中 rotate 属性用来设置小地图上锚点的旋转方向,position 属性用来设置锚点所处的位置,然后使用父组件传来的两者的值,使用 CSS 即可实现小地图锚点的位置和方向实时变化。
<template>
<div class="tiny-map">
<div class="map">
<i class="rotate"
:style="{
'transform': `rotate(${rotate}deg)`,
'left': `${position.left}px`,
'top': `${position.top}px`
}"
></i>
</div>
</div>
</template>
<script>
const props = defineProps({
rotate: {
type: Number,
default: 0,
},
position: {
type: Object,
default: () => ({ left: 0, top: 0 }),
},
});
</script>
至于 rotate 和 position 的获取,我们需要在父组件中这样来实现,rotate 即相机和轨道控制器之前形成的夹角度数,我们可以在动画方法 tick() 中,使用 camera 和controls 的 x轴 和 z轴 的参数,按下面公式计算出两者之间的夹角,并转换成子组件中需要的弧度类型。position 的获取就非常简单,我们先在 rooms 中配置好每个房间固定到 left 和 top 值,然后拿到当前房间的值,传给 TinyMap 组件即可。
tick() {
// ...
if (camera && controls) {
const dirx = camera.position.x - controls.target.x
const dirz = camera.position.z - controls.target.z
const theta = Math.atan2(dirx, dirz) * 180 / Math.PI;
}
}
小地图中锚点的位置随着看房视角的变化而旋转和移动。

整个在线看房 ? 项目的主要功能到这里已经全部介绍完了,实际项目中可能一个房源不止 5 个房间全景图,加之 HDR 全景图的体积也比较大,因此图片预加载和项目加载进度管理是非常有必要的,我们可以对其进一步进行优化,提升用户体验 ✨。

本文中主要包含的知识点包括:
Three.js 三维全景场景初始化想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 ?。
...本文作者: dragonir 本文地址 :https://www.cnblogs.com/dragonir/p/17301683.html
让我们计算MRI范围内的类别:defcount_classesObjectSpace.count_objects[:T_CLASS]endk=count_classes用类方法定义类:classAdefself.foonilendend然后运行:putscount_classes-k#=>3请解释一下,为什么是三个? 最佳答案 查看MRI代码,每次你创建一个Class时,在Ruby中它是Class类型的对象,ruby会自动为这个新类创建“元类”类,这是另一个单例类型的Class对象。C函数调用(class.c)是:rb_define
我开始了一个新的Rails3.2.5项目,Assets管道不再工作了。CSS和Javascript文件不再编译。这是尝试生成Assets时日志的输出:StartedGET"/assets/application.css?body=1"for127.0.0.1at2012-06-1623:59:11-0700Servedasset/application.css-200OK(0ms)[2012-06-1623:59:11]ERRORNoMethodError:undefinedmethod`each'fornil:NilClass/Users/greg/.rbenv/versions/1
在学习Python之后,我现在正在尝试学习Ruby,但我在将这段代码转换为Ruby时遇到了问题:defcompose1(f,g):"""Returnafunctionh,suchthath(x)=f(g(x))."""defh(x):returnf(g(x))returnh我必须使用block来翻译吗?或者Ruby中是否有类似的语法? 最佳答案 您可以使用Ruby中的lambda执行此操作(我在这里使用的是1.9stabby-lambda):compose=->(f,g){->(x){f.(g.(x))}}所以compose是一个返
rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:
我有一个包含多个组件的存储库,其中大部分是用JavaScript(Node.js)编写的,一个是用Ruby(RubyonRails)编写的。我想要一个.travis.yml文件来触发一个运行每个组件的所有测试的构建。根据thisTravisCIGoogleGroupthread,目前还没有官方支持。我的目录结构是这样的:.├──构建服务器├──核心├──扩展├──网络应用├──流浪文件├──package.json├──.travis.yml└──生成文件我希望能够运行特定版本的Ruby(2.2.2)和Node.js(0.12.2)。我已经有了一个make目标,所以maketest在每
前面一篇关于智能合约翻译文讲到了,是一种计算机程序,既然是程序,那就可以使用程序语言去编写智能合约了。而若想玩区块链上的项目,大部分区块链项目都是开源的,能看得懂智能合约代码,或找出其中的漏洞,那么,学习Solidity这门高级的智能合约语言是有必要的,当然,这都得在公链``````以太坊上,毕竟国内的联盟链有些是不兼容Solidity。Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态下的账户行为的程序。Solidity是运行在以太坊(Ethereum)虚拟机(EVM)上,其语法受到了c++、python、javascript影响。Solidity是静态类型
我在阅读有关.each迭代器的Ruby问题,有人说如果更高阶的迭代器更适合该任务,则使用.each可能会产生代码味道。Ruby中的高阶迭代器是什么?编辑:我提到的StackOverflow答案的作者JörgWMittag提到他是要编写更高级别的迭代器,但他还解释了下面的内容。 最佳答案 哎呀。我的意思是更高级别的迭代器,而不是更高阶的迭代器。当然,每个迭代器都是高阶的。基本上,迭代是一个非常低级的概念。编程的目的是与团队中的其他利益相关者交流意图。“初始化一个空数组,然后遍历另一个数组,并将该数组的当前元素添加到第一个数组中(如果该
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。多年来,我一直在使用多种语言进行编程,并且认为自己总体上相当擅长。但是,我从未编写过任何自动化测试:没有单元测试,没有TDD,没有BDD,什么都没有。我已经尝试开始为我的项目编写适当的测试套件。我可以看到在进行任何更改后能够自动测试项目中所有代码的理论值(value)。我可以看到像RSpec和Mocha这样的测试框架应该如何使设置和运行所述测试变得相当容易
我需要一些指导来了解如何将Angular整合到rails中。选择Rails的原因:我喜欢他们偏执的做事方式。还有迁移,gem真的很酷。使用angular的原因:我正在研究和寻找最适合SPA的框架。Backbone似乎太抽象了。我不得不在Angular和Ember之间做出选择。我首先开始阅读Angular,它对我来说很有意义。所以我从来没有去读过关于ember的文章。使用Angular和Rails的原因:我研究并尝试使用小型框架,例如grape、slim(是的,我也使用php)。但我觉得需要坚持项目的长期范围。我个人喜欢用Rails的方式做事。这就是我需要帮助的地方,我在Rails4中有
目标:我想从动画GIF中抓取最佳帧并将其用作静态预览图像。我相信最好的帧是显示最多内容的帧-不一定是第一帧或最后一帧。以这张动图为例:--这是第一帧:--这是第28帧:很明显,第28帧很好地代表了整个GIF。我如何以编程方式确定一帧是否比另一帧具有更多像素/内容?如果您能向我指出任何想法、想法、包/模块或文章,我们将不胜感激。 最佳答案 实现此目的的一种直接方法是估计entropy每个图像的帧,并选择具有最大熵的帧。在信息论中,熵可以被认为是图像的“随机性”。单一颜色的图像是非常可预测的,分布越平坦,越随机。这与Arthur-R描述