大家好,我是公众号「线下聚会游戏」作者HullQin,开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏。
DragEvent和TouchEvent。
我们在开发时,也要特别注意这点——这个交互要开发2次,同时支持DragEvent和TouchEvent。
PokerListSSQ,(其中SSQ是时少权的首字母,以他的名字做组件名,表示对创意提出者的尊重)。
selected和setSelected这两个东西维护在父组件中(可参考React文档:状态提升)。因此,这就多了2个参数:selected和setSelected。type PokerListProps = {
ids: number[];
height?: number;
className?: string;
selected: number[];
setSelected: number[] | (selected: number[]) => void;
style?: CSSProperties;
};
ids,有一个难点:如何把扑克牌按照预期摆放?
let cardNumber = getCardNumber(id);
cardNumber = cardNumber > 50 ? 50 : cardNumber;
其中getCardNumber会把扑克牌ID映射到扑克牌的一个值(代表它的大小)。3-13映射到3-13本身,A和2对应14、15,大王小王映射到54、53。
这里为了让大小王能够放在同一列展示,所以又做了一次转换,统一为50。
那么每个扑克牌的left距离计算如下:
let left;
if (cardNumber >= 50) left = 0;
else left = (16 - cardNumber) * gap;
其中gap就是相邻扑克牌的间距,可动态调整,本代码采用的是const gap = height * 48 / 159。
top是比较好计算的,也是等差数列,从0一直到7*padding(其中padding是垂直方向,两张相邻牌的间距,跟gap一个意思,只是一个横轴一个纵轴)。
但如果此时,如果你出了一张K,只有7个K了,而且其他牌不足8张。那么此时,所有牌的top都应该减去1个padding,保证上方没有太大空白。如果你的牌出到最后,中间留下7个padding的空白,是很丑的。
所以每张扑克牌的top不仅跟当前扑克牌是同数字牌中的第几张count有关,还跟最大相同牌数maxCount有关,公式如下:
const top = (maxCount - count) * padding;
效果如下:
出了1张8后,变为:
zIndex:
const zIndex = (left << 5) - count + 10;
left << 5就是乘了个很大的数字,也就是说,优先以left判断,left越小,表明位置越靠左,zIndex就小,应该被遮住。
对于同样大小的扑克牌,按照count计算,count越大,表明位置越靠上,zIndex越小,会被遮住。
<Poker
style={{
left, top, zIndex, filter: selected.includes(id - 1) ? 'brightness(0.8)' : 'brightness(1)', transform: `scale(${height / 159})`,
}}
/>
left top zIndex上面已经描述过。此外还用了filter给扑克牌增加黑色半透明遮罩层,用了transform给扑克牌放缩。
cardFlag,记录一开始点的牌,状态是什么。
const cardFlag = useRef<boolean>(false);
随后,给每个<Poker />添加事件onDragStart、onDragEnter:
onDragStart={(event: DragEvent) => {
if (event.dataTransfer) {
const img = new Image();
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
event.dataTransfer.setDragImage(img, 0, 0);
}
cardFlag.current = selected.includes(id - 1);
setSelected(((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(id - 1);
if (index2 === -1) {
if (!cardFlag.current) oldSelected.push(id - 1);
} else if (cardFlag.current) oldSelected.splice(index2, 1);
}));
}}
onDragEnter={() => {
setSelected(((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(id - 1);
if (index2 === -1) {
if (!cardFlag.current) oldSelected.push(id - 1);
} else if (cardFlag.current) oldSelected.splice(index2, 1);
}));
}}
div,需要给div设置draggable属性。如果你拖拽img、a这种天然支持拖拽的元素,就可以不用加。event.dataTransfer.setDragImage函数即可,设置了一个透明的拖拽图片。上面img.src是用base64构造了一个1*1的透明的gif。use-immer,所以setSelected的逻辑内可以直接修改oldSelected,而不必return newSelected。const [selectedCards, setSelectedCards] = useImmer<number[]>([]);
onTouch函数,它会被用2次,分别在onTouchStart、onTouchMove上。
const onTouch = (ev : TouchEvent) => {
const { clientX, clientY } = ev.changedTouches[0];
let topEl: HTMLElement | undefined;
let topZIndex = -999;
// TODO: 这里可以改用React ref引用,从而获取元素。调用dom API并不合理,但这看起来会容易懂。
Array.from(document.getElementsByClassName('my-poker-list')).forEach((el: any) => {
const {
x, y, width, height,
} = el.getBoundingClientRect();
if (clientX >= x && clientX <= x + width && clientY >= y && clientY <= y + height) {
const z = Number(el.style.zIndex);
if (z > topZIndex) {
topZIndex = z;
topEl = el;
}
}
});
// 上面计算到了当前触摸的扑克牌是哪张(topEl)
if (!topEl) return;
// 下面依赖dom元素的id属性获取扑克牌ID,所以需要给<Poker>增加id字段。
const currentId = Number(topEl.getAttribute('id')) - 1;
setSelected(((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(currentId);
if (index2 === -1) {
if (!cardFlag.current) oldSelected.push(currentId);
} else if (cardFlag.current) oldSelected.splice(index2, 1);
}));
};
给Poker赋值以下字段:
<Poker
key={id}
id={id}
className="my-poker-list"
onTouchStart={(ev: TouchEvent) => {
cardFlag.current = selected.includes(id - 1);
onTouch(ev);
}}
onTouchMove={(ev: TouchEvent) => {
onTouch(ev);
}}
/>
if ('ontouchstart' in window)即可。
onClick={() => {
if ('ontouchstart' in window) return;
setSelected((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(id - 1);
if (index2 === -1) {
oldSelected.push(id - 1);
} else {
oldSelected.splice(index2, 1);
}
});
}}
import React, {
CSSProperties, useEffect, useMemo, useRef,
} from 'react';
import Poker from './Poker';
import { getCardNumber, sortPokersById } from '../utils/ddz';
type PokerListProps = {
ids: number[];
height?: number;
className?: string;
selected: number[];
setSelected: any;
style?: CSSProperties;
};
function PokerListSSQ(props: PokerListProps) {
const {
ids: pids, height = 159, className, selected, setSelected, style,
} = props;
const ids = pids.map((i) => i + 1);
const sortedIds = useMemo(() => sortPokersById([...ids]), [ids]);
const cardFlag = useRef<boolean>(false);
useEffect(() => {
setSelected([]);
}, [sortedIds.length]);
const padding = height * 58 / 159;
const gap = height * 48 / 159;
let maxCount = 1;
let count = 0;
let lastCardNumber = 0;
sortedIds.forEach((id) => {
let cardNumber = getCardNumber(id);
cardNumber = cardNumber > 50 ? 50 : cardNumber;
if (cardNumber === lastCardNumber) {
count += 1;
if (count > maxCount) maxCount = count;
} else {
lastCardNumber = cardNumber;
count = 0;
}
});
count = 0;
lastCardNumber = 0;
const cards = sortedIds.map((id) => {
let cardNumber = getCardNumber(id);
cardNumber = cardNumber > 50 ? 50 : cardNumber;
if (cardNumber === lastCardNumber) {
count += 1;
} else {
lastCardNumber = cardNumber;
count = 0;
}
let left;
if (cardNumber >= 50) left = 0;
else left = (16 - cardNumber) * gap;
const onTouch = (ev : TouchEvent) => {
const { clientX, clientY } = ev.changedTouches[0];
let topEl: HTMLElement | undefined;
let topZIndex = -999;
Array.from(document.getElementsByClassName('my-poker-list')).forEach((el: any) => {
const {
x, y, width, height,
} = el.getBoundingClientRect();
if (clientX >= x && clientX <= x + width && clientY >= y && clientY <= y + height) {
const z = Number(el.style.zIndex);
if (z > topZIndex) {
topZIndex = z;
topEl = el;
}
}
});
if (!topEl) return;
const currentId = Number(topEl.getAttribute('id')) - 1;
setSelected(((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(currentId);
if (index2 === -1) {
if (!cardFlag.current) oldSelected.push(currentId);
} else if (cardFlag.current) oldSelected.splice(index2, 1);
}));
};
return (
<Poker
key={id}
id={id}
className="my-poker-list"
style={{
left, top: (maxCount - count) * padding, zIndex: (left << 5) - count + 10, filter: selected.includes(id - 1) ? 'brightness(0.8)' : 'brightness(1)', transform: `scale(${height / 159})`,
}}
onClick={() => {
if ('ontouchstart' in window) return;
setSelected((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(id - 1);
if (index2 === -1) {
oldSelected.push(id - 1);
} else {
oldSelected.splice(index2, 1);
}
});
}}
onDragStart={(event: DragEvent) => {
if (event.dataTransfer) {
const img = new Image();
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
event.dataTransfer.setDragImage(img, 0, 0);
}
cardFlag.current = selected.includes(id - 1);
setSelected(((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(id - 1);
if (index2 === -1) {
if (!cardFlag.current) oldSelected.push(id - 1);
} else if (cardFlag.current) oldSelected.splice(index2, 1);
}));
}}
onDragEnter={() => {
setSelected(((oldSelected: number[]) => {
const index2 = oldSelected.indexOf(id - 1);
if (index2 === -1) {
if (!cardFlag.current) oldSelected.push(id - 1);
} else if (cardFlag.current) oldSelected.splice(index2, 1);
}));
}}
onTouchStart={(ev: TouchEvent) => {
cardFlag.current = selected.includes(id - 1);
onTouch(ev);
}}
onTouchMove={(ev: TouchEvent) => {
onTouch(ev);
}}
/>
);
});
return (
<div
className={`poker-list${className ? ` ${className}` : ''}`}
style={{ height: height + padding * maxCount, ...style }}
>
{cards}
</div>
);
}
PokerListSSQ.defaultProps = {
height: 159,
};
export default PokerListSSQ;
注:
import Poker from './Poker';和import { getCardNumber, sortPokersById } from '../utils/ddz';的代码都在《展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!》。我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0
我的代码目前看起来像这样numbers=[1,2,3,4,5]defpop_threepop=[]3.times{pop有没有办法在一行中完成pop_three方法中的内容?我基本上想做类似numbers.slice(0,3)的事情,但要删除切片中的数组项。嗯...嗯,我想我刚刚意识到我可以试试slice! 最佳答案 是numbers.pop(3)或者numbers.shift(3)如果你想要另一边。 关于ruby-多次弹出/移动ruby数组,我们在StackOverflow上找到一
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
当我在我的Rails应用程序根目录中运行rakedoc:app时,API文档是使用/doc/README_FOR_APP作为主页生成的。我想向该文件添加.rdoc扩展名,以便它在GitHub上正确呈现。更好的是,我想将它移动到应用程序根目录(/README.rdoc)。有没有办法通过修改包含的rake/rdoctask任务在我的Rakefile中执行此操作?是否有某个地方可以查找可以修改的主页文件的名称?还是我必须编写一个新的Rake任务?额外的问题:Rails应用程序的两个单独文件/README和/doc/README_FOR_APP背后的逻辑是什么?为什么不只有一个?
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我在新的Debian6VirtualBoxVM上安装RVM时遇到问题。我已经安装了所有需要的包并使用下载了安装脚本(curl-shttps://rvm.beginrescueend.com/install/rvm)>rvm,但以单个用户身份运行时bashrvm我收到以下错误消息:ERROR:Unabletocheckoutbranch.安装在这里停止,并且(据我所知)没有安装RVM的任何文件。如果我以root身份运行脚本(对于多用户安装),我会收到另一条消息:Successfullycheckedoutbranch''安装程序继续并指示成功,但未添加.rvm目录,甚至在修改我的.bas
我从Ubuntu服务器上的RVM转移到rbenv。当我使用RVM时,使用bundle没有问题。转移到rbenv后,我在Jenkins的执行shell中收到“找不到命令”错误。我内爆并删除了RVM,并从~/.bashrc'中删除了所有与RVM相关的行。使用后我仍然收到此错误:rvmimploderm~/.rvm-rfrm~/.rvmrcgeminstallbundlerecho'exportPATH="$HOME/.rbenv/bin:$PATH"'>>~/.bashrcecho'eval"$(rbenvinit-)"'>>~/.bashrc.~/.bashrcrbenvversions
因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实
修改(澄清问题)我已经花了几天时间试图弄清楚如何从Facebook游戏中抓取特定信息;但是,我遇到了一堵又一堵砖墙。据我所知,主要问题如下。我可以使用Chrome的检查元素工具手动查找我需要的html-它似乎位于iframe中。但是,当我尝试抓取该iframe时,它是空的(属性除外):如果我使用浏览器的“查看页面源代码”工具,这与我看到的输出相同。我不明白为什么我看不到iframe中的数据。答案不是它是由AJAX之后添加的。(我知道这既是因为“查看页面源代码”可以读取Ajax添加的数据,也是因为我有b/c我一直等到我可以看到数据页面之后才抓取它,但它仍然不存在)。发生这种情况是因为
我遇到了一个非常奇怪的问题,我很难解决。在我看来,我有一个与data-remote="true"和data-method="delete"的链接。当我单击该链接时,我可以看到对我的Rails服务器的DELETE请求。返回的JS代码会更改此链接的属性,其中包括href和data-method。再次单击此链接后,我的服务器收到了对新href的请求,但使用的是旧的data-method,即使我已将其从DELETE到POST(它仍然发送一个DELETE请求)。但是,如果我刷新页面,HTML与"new"HTML相同(随返回的JS发生变化),但它实际上发送了正确的请求类型。这就是这个问题令我困惑的