第二个功能是我从另一个工具上参考来的,因为我根本不知道浏览器上还能这么操作。
\\n当调起 navigator.getDisplayMedia 这个接口的时候,\\n会弹起浏览器的系统弹窗:有三个可选项,分别是:Chrome 标签页,窗口,整个屏幕,选中某个界面进行分享之后,我们就能获取到对应的视频流。
\\n弹窗效果如下图所示:
\\n在 Web 中我们就可以通过创建 Video 标签将视频流渲染出来。最后怎么将视频转化为图片呢?熟悉 canvas 的不难,它有一个 drawImage 方法,可以将视频绘制在画布中。
\\n最后我们将 canvas 对象通过 toDataURL() 方法转化为图片数据编码显示在 img 标签中。
\\n原理就是这样,我们再来看下详细过程:
\\n通过 getDisplayMedia
获取到媒体流,然后在 DOM 中手动创建 video
标签,将媒体流渲染到视频组件中。然后从视频组件中截获某个帧,渲染到 canvas
\\n中来,以 drawImage
接口的形式拿到最终的图片二进制文件。
getDisplayMedia
在不同浏览器下的兼容性处理:
function getDisplayMedia(options) {
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
return navigator.mediaDevices.getDisplayMedia(options);
}
if (navigator.getDisplayMedia) {
return navigator.getDisplayMedia(options);
}
if (navigator.webkitGetDisplayMedia) {
return navigator.webkitGetDisplayMedia(options);
}
if (navigator.mozGetDisplayMedia) {
return navigator.mozGetDisplayMedia(options);
}
throw new Error(\'getDisplayMedia is not defined\');
}
看下 MDN 上改方法的用法:
\\nvar promise = navigator.mediaDevices.getDisplayMedia(constraints);
参数 constraints
的用法参考:
\\nhttps://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getDisplayMedia
它的返回值是一个被解析为 MediaStream 的 Promise 对象,其中包含一个视频轨道。
\\nasync function takeScreenshotStream() {
const width = screen.width * (window.devicePixelRatio || 1);
const height = screen.height * (window.devicePixelRatio || 1);
const errors = [];
let stream;
const mediaStreamConstraints = {
audio: false,
video: { width, height, frameRate: 1 }
};
try {
stream = await getDisplayMedia(mediaStreamConstraints);
} catch (ex) {
errors.push(ex);
}
if (errors.length) {
console.debug(...errors);
if (!stream) {
throw errors[errors.length - 1];
}
}
return stream;
}
这一步核心是配置参数,我们禁用音频,获取屏幕尺寸的和分辨率来设置视频的长宽。也就是最终捕获的图片的长宽。
\\n这里会主动向当前的文档中创建两个标签 video
和 canvas
标签,首先把上面获取到的视频流通过 video 标签渲染出来,视频数据加载出来后,再通过\\n创建的 canvas 的 drawImage
接口绘制到画布中。最终就对 canvas 对象通过 toDataURL() 方法转化为图片数据编码渲染到 img
标签上。
async function takeScreenshotCanvas() {
const stream = await takeScreenshotStream();
const video = document.createElement(\'video\');
const result = await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
video.play();
video.pause();
const canvas = document.createElement(\'canvas\');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext(\'2d\');
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
resolve(canvas);
}
video.srcObject = stream;
})
stream.getTracks().forEach(function (track) {
track.stop();
})
if (result == null) {
throw new Error(\'Cannot take canvas screenshot\');
}
return result;
}
这个方法中高亮的部分可能比较疑惑,它的作用是我们使用媒体流获取到当前的某个瞬间的视频流后,通过调用 stop
方法暂停所有轨道上数据的获取。其实我们截图\\n就只需要视频里某一帧的数据就可以了。关闭通道以免造成资源浪费。
通过流程图来解释就是这样:
\\n\\n不过它有个小小的缺点,就是点击分享后会离开当前页面,导致整个操作不连贯。在用户不知道该特性的情况下使用的时候会有点迷惑。
\\n不过这个功能还是大大简化了当你需要在其它页面或者窗口中截图来美化时,需要先隐藏当前窗口,然后手动调起截屏,选取截屏区域,保存截图后额外再点击上传图片。有了该功能,繁杂的流程简化了不少。
\\n欢迎大家将使用过程中的问题反馈出来,助力我将这个工具做的越来越好用。为有图片美化和设备套壳需求的人带来便利。
","description":"最近给图片套壳美化工具更新了两个重要的功能,简而言之就是: 直接操作图片的放大缩小\\n快速截图后上传图片(通过浏览器的 getDisplayMedia 接口)\\n直接操作放大缩小\\n\\n第一个功能是为了方便操作,之前是在左侧通过滑动区块来调节图片区域的显示大小,不够直观或不方便实现更精细化的调节。\\n\\n于是采用更常规的操作方式,在图片区域的四个角上新增操作手柄,当鼠标放在操作手柄上时,显示可拖拽的弧线。\\n\\n下图是对应的四个角的手柄位置和样式,和拖拽放大缩小的演示过程:\\n\\n\\n \\n\\n\\n关键代码\\n\\nexport default function Preview({ transf…","guid":"https://spacexcode.com/blog/screenshot-update-log","author":"fants0230@sina.com (编程范儿)","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-09-19T00:00:00.072Z","media":[{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/mp4/screenshot-update-log.mp4","type":"video","width":100},{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1726726566371-7fd8c312-3769-4e8f-b62e-91f747c2003f.png","type":"photo","width":1176,"height":965,"blurhash":"LMQbt#|TulMx^}OtIAngyqJpyVkX"}],"categories":["截图","拖拽放大","演示"],"attachments":null,"extra":null,"language":null},{"title":"对滑块组件 Slider 样式魔改","url":"https://spacexcode.com/blog/customize-slider","content":"最近在浏览网站的时候看到一个有趣的滑动组件的样式,觉得很有趣。一时有兴趣就去用代码实现了一下:
\\n\\n\\n这里我就不从零开始实现了,借用 Material UI Slider 快速实现,基本上就是重新写样式覆盖。
\\nMaterial UI 上的 Slider
组件默认样式如下:
import Slider from \'@mui/material/Slider\';
export default function Main() {
<Slider
aria-label=\\"Temperature\\"
defaultValue={30}
color=\\"secondary\\"
/>
}
很明显,这个和上面视频中的组件在样式上相差甚远。
\\n首先我们来剖析下组成这个组件所需要的元素有哪些?打开浏览器的元素审查,我们找到和 Slider 组件相关的元素标签,会发现主要结构如下:
\\n<span class=\\"MuiSlider-root\\">
<span class=\\"MuiSlider-rail\\"></span>
<span class=\\"MuiSlider-track\\"></span>
<span class=\\"MuiSlider-thumb\\"></span>
</span>
这里略去一些无关紧要的元素,主要就是根元素下的类名为 MuiSlider-rail
、MuiSlider-track
和 MuiSlider-thumb
的三个 span 标签组成。\\n接下来我们也是主要围绕这三个元素对它们的进行样式进行重写。
接下来的文章内容会通过逐步地修改,展示修改后的样式。
\\n得益于 Material UI 组件的 sx
属性,可以自由地对组件的样式进行覆盖式重写。sx
的值是一个 CSS 对象,可以做到对深层子元素的样式定义。
<Slider
sx={{
height: 28
}}
/>
对于滑块的颜色只要设置滑动组件的根元素的 color
属性即可,它的子元素通过设置 currentColor
直接继承过来。\\n滑块底色通过设置一定的透明度来区分已滑过区域和未滑过的区域。
<Slider
sx={{
height: 28,
color: \'rgba(220,220,220,1)\'
}}
/>
稍稍改变下圆角的值,继续在 sx
下追加属性 borderRadius: \'5px\'
。
原来的拖拽按钮是一个圆形的,我们要将它改为细长条样式。
\\n<Slider
sx={{
height: 28,
color: \'rgba(220,220,220,1)\',
borderRadius: \'5px\',
\'& .MuiSlider-thumb\': {
width: \'2px\',
height: \'20px\',
margin: \'0 4px\',
borderRadius: 0,
backgroundColor: \'currentColor\',
boxShadow: \'none\',
border: \'none\',
\'&::before\': { boxShadow: \'none\' },
}
}}
/>
对于文字的显示,我们在滑动组件同级,新增元素标签包裹,然后采用绝对定位,让它离开当前的文档流浮动在组件的上方。
\\n\\n最后,完整代码如下:
\\nimport { Box, Slider } from \'@mui/material\';
export default function CustomSlider() {
const [score, setScore] = useState(45);
return (
<Box sx={{ position: \'relative\', display: \'flex\' }}>
<Slider
defaultValue={score}
aria-label=\'Score\'
min={1}
step={1}
max={100}
sx={{
height: 28,
color: \'rgba(220,220,220,1)\',
borderRadius: \'5px\',
\'& .MuiSlider-thumb\': {
width: \'2px\',
height: \'20px\',
margin: \'0 4px\',
borderRadius: 0,
backgroundColor: \'currentColor\',
boxShadow: \'none\',
border: \'none\',
\'&::before\': { boxShadow: \'none\' },
},
\'& .MuiSlider-rail\': {
position: \'relative\'
},
\'& .MuiSlider-track\': {
border: \'none\'
}
}}
onChange={(e, val) => setScore(val)}
/>
<Box sx={{
width: \'100%\',
display: \'flex\',
position: \'absolute\',
height: \'100%\',
top: 0,
zIndex: 1,
alignItems: \'center\',
justifyContent: \'space-between\',
padding: \'0 8px\',
pointerEvents: \'none\',
fontSize: \'11px\',
color: \'rgba(0,0,0,0.56)\'
}}>
<span>Score</span>
<span>{score}</span>
</Box>
</Box>
)
}
如果你有兴趣,可以根据原理来实现其它样式的滑块组件。
","description":"最近在浏览网站的时候看到一个有趣的滑动组件的样式,觉得很有趣。一时有兴趣就去用代码实现了一下: 这里我就不从零开始实现了,借用 Material UI Slider 快速实现,基本上就是重新写样式覆盖。\\n\\nMaterial UI 上的 Slider 组件默认样式如下:\\n\\n\\n \\n\\nimport Slider from \'@mui/material/Slider\';\\n\\nexport default function Main() {\\n连续做了几款图片导出工具后,这次决定做一个复杂点的,覆盖场景更丰富点的图片美化工具。
\\n为什么需要图片美化工具?
\\n日常分享到社交媒体的图片如果不加修饰,显示效果不佳。而且还可能因为图片尺寸不合适,导致显示不全。如果我们随意截图发到朋友圈或者社交平台,\\n导致图片大小不一致,排列错乱,毫无美感。更不会引起别人的关注。特别是对于品牌的宣传,统一的图片风格更容易在视觉上加深对受众的印象。
\\n结合前面的经验,技术实现上已经没什么障碍了。\\n这种产品的唯一的挑战是 UI 设计,和部分功能的 UX。产品的审美一定要在线,才能吸引别人来使用你的工具。
\\n名称 | 链接 | 图片 |
---|---|---|
封面图生成 | spacexcode.com/coverview | ![]() |
文字卡片生成 | spacexcode.com/memocard | ![]() |
代码图片生成 | spacexcode.com/codeimage | ![]() |
所以我也是借鉴各种已有的相关产品,前期做了很多的调研。才渐渐确定了目前的软件设计。经常多次迭代,算是 1.0 版本吧,有了多款模型之后,\\n决定正式发布上线,对外推广。
\\n这个产品的核心就是提供不同设备和场景下的模型,根据用户的喜好配置参数,然后导出适用不同平台尺寸的图片。
\\n整个界面分为三大块,按左 - 中 - 右的布局,页面全屏显示,不出现滚动条。左边为模型和框架的选择、参数的配置区域,中间是预览和一些功能\\n按钮,右边是对图片的布局和变换样式的选择。
\\n常见图片美化工具都有哪些功能?
\\n相较于市面上其它产品,我的工具加了一些亮点功能:
\\n后续迭代计划:
\\n独立开发一款产品真的很费心力,需要充当多面手。但是如果做成之后收到大家的关注和使用,感觉一定会很棒!这也是支撑我一路做下来的动力。
\\n先做成一件事,这是我接下来不断付诸实践的目标。如果你也在从事开发工作,对自己的职业很迷茫,可以看看这篇文章:W.I.N第二曲线手册。\\n也许会有所收获。
","description":"博客已经两个月没有更新了,我去干啥了?这次不是偷懒,去搞了个大活! 连续做了几款图片导出工具后,这次决定做一个复杂点的,覆盖场景更丰富点的图片美化工具。\\n\\n为什么需要图片美化工具?\\n\\n日常分享到社交媒体的图片如果不加修饰,显示效果不佳。而且还可能因为图片尺寸不合适,导致显示不全。如果我们随意截图发到朋友圈或者社交平台, 导致图片大小不一致,排列错乱,毫无美感。更不会引起别人的关注。特别是对于品牌的宣传,统一的图片风格更容易在视觉上加深对受众的印象。\\n\\n结合前面的经验,技术实现上已经没什么障碍了。 这种产品的唯一的挑战是 UI 设计,和部分功能的 UX…","guid":"https://spacexcode.com/blog/build-screenshot-tool","author":"fants0230@sina.com (编程范儿)","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-08-01T00:00:00.949Z","media":[{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1709197073172-42cb76bd-9cfc-4cbb-be54-4e654657a66e.png","type":"photo","width":810,"height":405,"blurhash":"L9RfnOIE4q-;M{M_j@xuD{xos;IV"},{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1722505197697-86c55814-d2d2-431c-ad88-1cbf37201054.png","type":"photo","width":810,"height":373,"blurhash":"LNNKSgaxD+ogWBWBofay04ogt6WB"},{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1722505333884-1d51f616-898e-4a1b-adbf-697863d9a904.png","type":"photo","width":680,"height":342,"blurhash":"LUD7Ct,l,yr;,to0awfR=8nhSbkC"},{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1722505827454-43547ea6-396c-4e03-9c35-87bc92607cac.png","type":"photo","width":1915,"height":930,"blurhash":"LJQla]?@.8UG=qf,Oss9$RNHJ$i_"}],"categories":["图片美化","图片工具","截图美化"],"attachments":null,"extra":null,"language":null},{"title":"技术博客的内容形式发展趋势","url":"https://spacexcode.com/blog/the-change-of-blog","content":"你平时都在什么平台写博客?你写博客时的编辑器是什么?
\\n依我观察,现在的博客形式无外乎以下几种:
\\n不管平台或工具是什么?它们或是因为有良好的撰写体验,或是易于在线发布和传播,抑或对搜索引擎友好且社区氛围好易于扩大影响力等原因。
\\n今天我们不讨论用哪个平台,何种工具来生产和呈现你的博客。着重关注下博客的内容形式随着 Web 技术的发展带来了哪些变化?
\\n我们最初的印象中一篇文章的形成素材,无非文字搭配图片,这也是从纸媒时代延续过来最常见的方式。之后过度到互联网时代,随着多媒体素材逐渐丰富起来,于是文章中也有了视频、GIF、SVG 等元素,\\n文章给人的体验也从单纯的视觉扩大到听觉。我们接受的信息维度发生了变化。
\\n这种变化的感受对于经历完整互联网发展的我们来说,或者并不算深刻。但是,接下来的要讨论的这种趋势,你不得不去关注。🕵️♂️
\\n你可以打开下面两篇文章观察下:
\\n发现两个作者在讲解知识的时候,都配有大量的生动案例,通过一系列的鼠标或键盘操作来和读者的教程进行互动。来达到读者在观察网页上一些元素的样式和行为发生的变化过程中思考背后的原理的目的。
\\n很重要的一点:我们已经从单纯被动接受信息,到一种参与互动,仿佛在跟着文章思维和作者进行交流。这种学习的过程更加立体和深刻。
\\n另外通过这种方式写作,更能锻炼写作者对某个知识点背后的原理和在实际场景中的应用的深入理解。因为每个案例都是该知识点在实际应用中的一种折射。
\\n我理解这种创作的深度比单纯的码文字和贴图要高的多。
\\n而之所以有这种变化的趋势,我觉得背后得益于 Web 技术的发展的助推,静态网站生成技术对 mdx
格式的文件的支持:同一个文件中不仅支持 md
语法,还能直接写 jsx
代码在文件中执行。
\\n\\n让 Markdown 步入组件时代
\\n
\\nMDX 将 markdown 和 JSX 语法完美地融合在一起,完美地适配 基于 JSX 的项目,亦可将现有的组件使用到 MDX 中,并且可以将其它 MDX 文件作为组件导入。\\n保留了 markdown 的简洁和优雅, 你只需在需要时使用 JSX。
我们知道有这种变化之后,不妨继续探索下,可以从哪些方面去实践?
\\n首先我们从交互形式的角度分析:
\\n下面我们来通过几个真实的案例,具体看看都有哪些可能性?
\\n通过键盘按键互动
\\n\\n\\n\\n这是一种常见的应用场景,通过监听键盘大小键的关闭和开启来给与用户输入密码时友好提示,避免大小写切换时用户感知上的迷惑。
\\n核心代码如下 👇:
\\nwindow.addEventListener(\'keydown\', detectCapsLock)
window.addEventListener(\'keyup\', detectCapsLock)
function detectCapsLock(e) {
if (e.getModifierState(\'CapsLock\')) {
// caps lock is on
} else {
// caps lock is off
}
}
我们可以通过这种真实场景的模拟,来感受下隐藏在背后的技术的实现过程,比单纯的代码和文字讲解要更生动,更能激发人的探索欲。
\\n及时编码,实时展示效果
\\n这是一段代码块的 live
模式,依赖于开源库 https://github.com/FormidableLabs/react-live 实现。使得可以同时通过可编辑的源代码和实时预览两种模式来渲染 React 组件。
\\n\\n修改代码块中防抖函数中的数值来观察结果区文字变化的响应速度
\\n
function App() {\\n const [mouseX, setMouseX] = React.useState(null);\\n const [mouseY, setMouseY] = React.useState(null);\\n\\n const handleMouseMove = React.useMemo(\\n () => debounce((ev) => {\\n setMouseX(ev.clientX);\\n setMouseY(ev.clientY);\\n }, 250), // 修改防抖数值观察变化\\n []\\n );\\n \\n return (\\n <div\\n style={{ padding: \'40px 20px\', border: \'2px dashed #ebedf0\' }}\\n onMouseMove={handleMouseMove}>\\n 鼠标位置:{ mouseX ? `X: ${mouseX}` : \'\' } { mouseY ? `Y: ${mouseY}` : \'\' }\\n </div>\\n );\\n}\\n
一边写代码,一边展示效果,有点类似本地的网页开发体验。
\\n这种及时显示代码效果的的互动形式,大大提高了读者对代码的兴趣。
\\n相比在文章中附上 CODEPEN(https://codepen.io) 这种代码演示平台的链接。避免了阅读时文章和代码试验区的切换的割裂感,完美与正文集成在一起,\\n既保证了顺畅的阅读体验,还能让我们体验在线调试代码的过程。
\\n通过改变配置,刷新页面效果
\\n在学习 Flex 布局时,相比单纯介绍各个属性的含义和用法。如果通过这样一个真实的案例:更改下面盒子的宽度,观察表单的布局和样式的变化:
\\n\\n600px
form {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 16px;
}
.name {
flex-grow: 1;
flex-basis: 160px;
}
.email {
flex-grow: 3;
flex-basis: 200px;
}
button {
flex-grow: 1;
flex-basis: 80px;
}
先通过案例让你产生兴趣,顺便思考背后的技术的实现。然后再点开查看代码,这样的教程是不是更有意思?
\\n这种强交互的文章有哪些产品方面的应用,做的最好的应该是公众号,我们通常无法拒绝那种通过点击展开图片,或点击变换场景的公众号推文。它深刻抓住了互动在营销上传播力。
\\n把握趋势,追逐趋势。这也是我今后在这个博客上持续发力进行创作的方向。
\\n不知道看完文章的分析,是不是让你对博客的呈现形态有了新的认识,你会认同我的想法吗?欢迎评论区给与您的意见。
","description":"你平时都在什么平台写博客?你写博客时的编辑器是什么? 依我观察,现在的博客形式无外乎以下几种:\\n\\n本地编辑器书写,Hexo、Hugo、Vitepress 等部署\\nNotion、语雀和飞书等工具记录和发布\\nWordPress 等开源网站程序搭建,在线书写和发布\\nGithub 仓库或 Page\\n掘金、知乎、CSDN等在线平台记录\\n\\n不管平台或工具是什么?它们或是因为有良好的撰写体验,或是易于在线发布和传播,抑或对搜索引擎友好且社区氛围好易于扩大影响力等原因。\\n\\n今天我们不讨论用哪个平台,何种工具来生产和呈现你的博客。着重关注下博客的内容形式随着 Web…","guid":"https://spacexcode.com/blog/the-change-of-blog","author":"fants0230@sina.com (编程范儿)","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-05-28T00:00:00.949Z","media":[{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1716857763257-def4ca56-a2f3-4cce-a7dd-3c346f953f2a.png","type":"photo","width":1446,"height":887,"blurhash":"LeQJyIWC%Mxu?wxtM{of9FRjxuWB"},{"url":"https://spacexcode.oss-cn-hangzhou.aliyuncs.com/1716857763281-3fdb52ab-0f82-4d36-b178-da58d3d2c4d9.png","type":"photo","width":1446,"height":887,"blurhash":"LJQTGw9F_3-;yZ-;9FIU4;Rkt7of"}],"categories":["教程","动态演示","博客","发展趋势"],"attachments":null,"extra":null,"language":null}],"readCount":10,"subscriptionCount":32,"analytics":{"feedId":"63934604555150336","updatesPerWeek":null,"subscriptionCount":32,"latestEntryPublishedAt":null,"view":0}}')