做一个独一无二的重力版个人博客

rome753.github.io
访问博客

0 起因

一直在简书上写博客,书写和传图很方便,缺点是个人主页太过简陋。我试了一下个人博客Hexo,发现虽然功能很强大,可定制性也很高,然而定制起来也很麻烦,大部分只能改改颜色样式排版等。我想做的是一个独一无二个人博客页面,比如有树状图、物理碰撞或者WebGL 3D效果等。

先做个物理碰撞的页面试试:每篇博客标题是一个矩形方块,可以用鼠标拖动、有坠落碰撞反弹等。

1 box2d.js研究

以前研究过Box2D物理引擎,在Github上找了一下网页版的box2d.js,其实它是用Emscripten将Box2D的C++代码直接编译成JavaScript的,使用方法基本跟Box2D一样。看了一下它的testbed实例,效果不错,可以满足我的要求。

把box2d.js下载下来,用VSCode打开,我们需要的代码在demo/html5canvas里面。
截屏2022-07-14 11.00.05.png

搜索安装一下Live Preview插件,注意是微软官方开发的,它可以在预览网页时自动启动一个http服务器,这样就能正常预览网页了。打开testbed.html文件,点右上角的图标打开预览,就能在开发中实时预览,极其方便!

研究一下这个Demo项目:

  • testbed.html
    绘制的画布就是这个canvas,可以修改宽高等。

    <body>
        <div style="text-align:center">
            <h2>Emscripten Box2D demo</h2>

            <div style="margin:auto;width:640px;padding:2px;border:1px solid #888;text-align:left">            
            
                <!--<canvas id="canvas" width="480" height="320" tabindex='1'></canvas>-->
                <canvas id="canvas" width="640" height="480" tabindex='1'></canvas>
  • test目录
    这里面是Box2D的测试集合,每个文件代表一个测试,每个测试里主要是在setup方法里创建Box2D模型。
embox2dTest_fallingShapes.prototype.setup = function() {

    //ground edges
    var shape0 = new b2EdgeShape();
    shape0.Set(new b2Vec2(-40.0, -6.0), new b2Vec2(40.0, -6.0));
    groundBody.CreateFixture(shape0, 0.0);
    shape0.Set(new b2Vec2(-9.0, -6.0), new b2Vec2(-9.0, -4.0));
    groundBody.CreateFixture(shape0, 0.0);
    shape0.Set(new b2Vec2(9.0, -6.0), new b2Vec2(9.0, -4.0));
    groundBody.CreateFixture(shape0, 0.0);

    NUMRANGE.forEach(function(i) {
        var bd = new b2BodyDef();
        // bd.set_type(b2_dynamicBody);
        bd.set_type(Module.b2_dynamicBody);
        bd.set_position(ZERO);
        var body = world.CreateBody(bd);
        var randomValue = Math.random();
        if ( randomValue < 0.2 )
            body.CreateFixture(cshape, 1.0);
        else
            body.CreateFixture(createRandomPolygonShape(0.5), 1.0);
        temp.Set(16*(Math.random()-0.5), 4.0 + 2.5*i);
        body.SetTransform(temp, 0.0);
        body.SetLinearVelocity(ZERO);
        body.SetAwake(1);
        body.SetActive(1);
    });

  • embox2d-html5canvas-testbed.js
    这个类是最重要的功能所在,分为创建World、绘制和处理鼠标时间三部分。

创建World和绘制


function createWorld() {
    
    if ( world != null ) 
        Box2D.destroy(world);
        
    world = new Box2D.b2World( new Box2D.b2Vec2(0.0, -10.0) );
    world.SetDebugDraw(myDebugDraw);
    
    mouseJointGroundBody = world.CreateBody( new Box2D.b2BodyDef() );
    
    var e = document.getElementById("testSelection");
    var v = e.options[e.selectedIndex].value;
    
    eval( "currentTest = new "+v+"();" );
    
    currentTest.setup();
}

function resetScene() {
    createWorld();
    draw();
}

function step(timestamp) {
    
    if ( currentTest && currentTest.step ) 
        currentTest.step();
    
    if ( ! showStats ) {
        world.Step(1/60, 3, 2);
        draw();
        return;
    }
    
    var current = Date.now();
    world.Step(1/60, 3, 2);
    var frametime = (Date.now() - current);
    frameTime60 = frameTime60 * (59/60) + frametime * (1/60);
    
    draw();
    statusUpdateCounter++;
    if ( statusUpdateCounter > 20 ) {
        updateStats();
        statusUpdateCounter = 0;
    }
}

function draw() {
    
    //black background
    context.fillStyle = 'rgb(0,0,0)';
    context.fillRect( 0, 0, canvas.width, canvas.height );
    
    context.save();            
        context.translate(canvasOffset.x, canvasOffset.y);
        context.scale(1,-1);                
        context.scale(PTM,PTM);
        context.lineWidth /= PTM;
        
        drawAxes(context);
        
        context.fillStyle = 'rgb(255,255,0)';
        world.DrawDebugData();
        
        if ( mouseJoint != null ) {
            //mouse joint is not drawn with regular joints in debug draw
            var p1 = mouseJoint.GetAnchorB();
            var p2 = mouseJoint.GetTarget();
            context.strokeStyle = 'rgb(204,204,204)';
            context.beginPath();
            context.moveTo(p1.get_x(),p1.get_y());
            context.lineTo(p2.get_x(),p2.get_y());
            context.stroke();
        }
        
    context.restore();
}

注意:DebugDraw的绘制是在helpers/embox2d-html5canvas-debugDraw.js这个类里,调试的时候一定要开启DebugDraw,不然有时找不到自己添加的物体跑哪里去了。DebugDraw把物体分解成点和线了,只能用来绘制物体轮廓,要绘制真正的图形需要遍历World里的body集合,根据body的位置和旋转角度绘制。

处理鼠标事件
通过QueryAABB方法找到鼠标事件在World里的点,创建mouseJoint用来拖动物体。


function startMouseJoint() {
    
    if ( mouseJoint != null )
        return;
    
    // Make a small box.
    var aabb = new Box2D.b2AABB();
    var d = 0.001;            
    aabb.set_lowerBound(new b2Vec2(mousePosWorld.x - d, mousePosWorld.y - d));
    aabb.set_upperBound(new b2Vec2(mousePosWorld.x + d, mousePosWorld.y + d));
    
    // Query the world for overlapping shapes.            
    myQueryCallback.m_fixture = null;
    myQueryCallback.m_point = new Box2D.b2Vec2(mousePosWorld.x, mousePosWorld.y);
    world.QueryAABB(myQueryCallback, aabb);
    
    if (myQueryCallback.m_fixture)
    {
        var body = myQueryCallback.m_fixture.GetBody();
        var md = new Box2D.b2MouseJointDef();
        md.set_bodyA(mouseJointGroundBody);
        md.set_bodyB(body);
        md.set_target( new Box2D.b2Vec2(mousePosWorld.x, mousePosWorld.y) );
        md.set_maxForce( 1000 * body.GetMass() );
        md.set_collideConnected(true);
        
        mouseJoint = Box2D.castObject( world.CreateJoint(md), Box2D.b2MouseJoint );
        body.SetAwake(true);
    }
}

2 抓取博客记录

我要获取自己在简书的全部博客记录,主要是每一篇的标题和链接。标题用来放进Box2D中,链接用来点击跳转。搜了一下,简书有非公开的api可以拉取。

https://blog.csdn.net/weixin_40508682/article/details/88577733
这篇文章讲到了具体接口。

首先拉取全部文章,然后把标题处理成图片,最后在网页里读取图片和其他信息。前两步用python处理。

blogRequest.py

  • 拉取文章
    用python循环拉取分页,并保存到文件中

def requestSaveFile():
    f = open(jsonPath, 'w')
    headers = {
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"    
    }
    hasClose = False
    for i in range(1, 20):
        url = # 见完整代码
        r = requests.get(url, headers=headers)
        jo = json.loads(r.content)
        f.write(r.text)
        f.write('\n')
        print("write page: %d" % i)
        if (len(jo) == 0):
            print("close file")
            f.close()
            hasClose = True
            break
        # print(jo[0]['object']['data'])
        # print(i)
    if hasClose == False:
        f.close()
  • 标题转换成图片
    用PIL库进行python绘图,注意中文字体需要提供系统的字体库,mac系统是在/Library/Fonts/里面,测量文字宽高,然后绘图,顺便设置颜色边框等。将图片保存到目录里。

def createImage(fpath, text):
    global createCount
    fontSize = maxFontSize - int(createCount / 10)
    if (fontSize < 25):
        fontSize = 25
    createCount += 1
    fontPath = '/Library/Fonts/Arial Unicode.ttf' # 系统字体
    font = ImageFont.truetype(fontPath, fontSize)
    textSize = font.getsize(text)
    
    pd = 10
    w = textSize[0] + pd + pd
    h = textSize[1] + pd + pd

    fontColor = '#000000'
    if (text.startswith('Android')):
        fontColor = '#009933'
    if (text.startswith('iOS')):
        fontColor = '#996633'
    if (text.startswith('FFmpeg')):
        fontColor = '#6600cc'
    if (text.startswith('OpenCV')):
        fontColor = '#993399'

    im = Image.new("RGBA", [w, h], (255,255,255,0))
    dr = ImageDraw.Draw(im)

    dr.rounded_rectangle(xy=[0,0,w,h], radius=8, fill='#ffffff', outline='#dddddd', width=2)
    dr.text((pd, pd), text, font=font, fill=fontColor)
    im.save(fpath)

3 Box2D中添加图片

我需要的效果跟Demo里面tests/fallingShapes.js这个测试非常像,因此新建一个测试叫fallingImages.js,在里面创建物体。

  • 添加物体

读取前面创建的标题图片,根据图片的宽高创建Shape,调用body SetUserData方法,将id放进去,后面遍历World里面的body时会用到。注意将image缓存到Map里,避免重复解析。


function addImageBody(id) {
    var image = new Image()
    image.src = myBlogJson[id]['path']
    image.onload = function() {
        var w = image.width / myImageScale
        var h = image.height / myImageScale
        var ZERO = new b2Vec2(0, 0);
        var temp = new b2Vec2(0, 0);
        var bd = new b2BodyDef();
        // bd.set_type(b2_dynamicBody);
        bd.set_type(Module.b2_dynamicBody);
        bd.set_position(ZERO);
        var body = world.CreateBody(bd);
        var randomValue = Math.random();

        if (id == 753) {
            var shape = new b2CircleShape();
            console.log(shape);
            shape.set_m_radius(w / 2);
            body.CreateFixture(shape, 1);
        } else {
            var shape = new b2PolygonShape();
            shape.SetAsBox(w / 2, h / 2);
            body.CreateFixture(shape, 1);
        }

        temp.Set(22*(Math.random()-0.5), 12);
        body.SetTransform(temp, 0.0);
        body.SetLinearVelocity(ZERO);
        // body.SetAngle
        body.SetAwake(1);
        body.SetActive(1);

        body.SetUserData(id);
        myBlogImages.set(id, image)
    }
}
  • 绘制图片

这是最重要的一步

用world.GetBodyList和body = body.GetNext()遍历,注意要用body.a == 0来判断是否结束,否则会陷入死循环!

根据body.GetUserData()方法获取前面放进去的id,找到对应的图片image,用body.GetWorldCenter找到物体中心点,用body.GetAngle()找到图片旋转角度。注意JavaScript只能旋转画布,想旋转图片需要先save,将画布平移到图片中心点,旋转完再平移回来restore。


function draw() {
    
    //black background
    context.fillStyle = 'rgb(255,255,255)';
    context.fillRect( 0, 0, canvas.width, canvas.height );
    
    context.save();            
        context.translate(canvasOffset.x, canvasOffset.y);
        context.scale(1,-1);                
        context.scale(PTM,PTM);
        context.lineWidth /= PTM;
        
        // drawAxes(context);
        drawImage();
        
        context.fillStyle = 'rgb(255,255,0)';
        // world.DrawDebugData();
        
        if ( mouseJoint != null ) {
            //mouse joint is not drawn with regular joints in debug draw
            var p1 = mouseJoint.GetAnchorB();
            var p2 = mouseJoint.GetTarget();
            context.strokeStyle = 'rgb(204,204,204)';
            context.beginPath();
            context.moveTo(p1.get_x(),p1.get_y());
            context.lineTo(p2.get_x(),p2.get_y());
            context.stroke();
        }

    context.restore();
}

function drawImage() {
    var body = world.GetBodyList();
    while (true) {
        if (body == null || body.a == 0) {
            break;
        }
        var id = body.GetUserData();
        if (id > 0) {
            var c = body.GetWorldCenter();
            var w = myBlogImages.get(id).width / myImageScale;
            var h = myBlogImages.get(id).height / myImageScale;
            var a = body.GetAngle();

        
            // 旋转不在中心点,先平移再平移回来
            context.save();              
            context.translate(c.x, c.y);
            context.scale(1,-1);  
            context.rotate(-a);
            context.translate(-c.x, -c.y);
            var image = myBlogImages.get(id)
            context.drawImage(image, c.x - w / 2, c.y - h / 2, w, h);
            context.restore();
        }
        body = body.GetNext();
    }
}
  • 添加点击事件
    在onMouseDown和onMouseUp方法处理,由于有拖动事件,所以要根据点击时长和移动距离判断是不是真正的点击。用window.open(link)打开文章链接。

var clicktime = 0;
var clickx = 0;
var clicky = 0;

function onMouseDown(canvas, evt) {            
    updateMousePos(canvas, evt);
    if ( !mouseDown )
        startMouseJoint();
    mouseDown = true;
    updateStats();

    clicktime = Date.now();
    clickx = evt.clientX;
    clicky = evt.clientY
}

function onMouseUp(canvas, evt) {
    mouseDown = false;
    updateMousePos(canvas, evt);
    updateStats();
    if ( mouseJoint != null ) {
        var dx = evt.clientX - clickx;
        var dy = evt.clientY - clicky;
        var dis = dx * dx + dy * dy;
        if ((Date.now() - clicktime) < 150 && dis < 10) {
            var id = mouseJoint.GetBodyB().GetUserData();
            var link = myBlogJson[id]['link']
            window.open(link);
            console.log(link);
        }

        world.DestroyJoint(mouseJoint);
        mouseJoint = null;
    }
}
posted @ 2022-07-19 20:15  rome753  阅读(170)  评论(0编辑  收藏  举报