Blog logo

Blog logoJustZht

Justin Fincher

2020.1.23

 • 

Vortex 已经开源了。自己对 Unity-Android LWP 的实现,UniLWP,也会近期开源,估计会和功能加强版的 Asset Store 链接同一时间发布。要说 UniLWP 有什么好的,就是相较于其他的方案,UniLWP 直接从 Unity 2019.3 开始支持,不会强制 Activity 作为 initializer 的 context,基础版只有一个 18kb 的 jar,集成没有侵入性(因为用了 ContextProvider 做提前初始化然后用 Application 替代 Activity 来作为 context init,然后 merge 后的 manifest xml 已经去除了 Unity 自带的 Activity 改为了我的 Activity 实现),基础版不需要自定义 Native UI 的话直接集成到 Unity 默认打包 pipeline,以及兼容 CloudBuild 吧。

2020.1.16

 • 

做作品集的时候发现 Skyline 的官网在 Safari 上会重定向的报错,于是又花了两个小时去改网页,把 Vue CLI 和 Balm UI 都升级到了 4.x。Balm UI 的 Material Design 组件库相比 1.x 时终于算是比较齐全了,因此把之前的左上角的弹出菜单改成了侧滑菜单。不过 three.js 的部分一直没有从老的仓库里迁移过来,因此 Presents Viewer 的部分还是半废的。
Skyline 2.x 从建立仓库起就算是半个 monorepo,因为总结构是 Unity 的 Assets / Settings 那一套,但是同级别还有个 Builds 目录,用来放 Android 工程和 Firebase 的前端及云函数工程,在这种项目上操作就有种横冲直撞的感觉。


买了 fingameworks.com,目前和 fingame.works 一样都跳转到很久之前做的 GitHub Pages。申请季结束后开始做 Epoch 估计也会更新一下 FinGameWorks 的主页。


photo_2020-01-16-21.22.24
在家人的帮助下装好了支架,工作台的显示器不占用桌面空间了,支架真是提升生产力和幸福度的东西啊。


听了 HONDA - Friday Night Plans, Plastic Love - Friday Night PlansDuñe x Crayon - PointlessTatsuro Yamashita (山下達郎) - MOONGLOW

2020.1.7

 • 

最近在做 PDF 版本的 Portfolio,因为申请的有一个游戏设计,没办法填写我的在线 Portfolio。
一开始想按照 Lackar 的风格来做,不过到后面索性放飞自我了,配色相比之前也用的比较大胆,不过效果我还算比较满意,放几张图:

2000


给 Flutter Clock Challenge 做了一个彩色小钟,套路和 Vortex 其实差不多,都是根据数据更新颜色。做这个的时候试用了下 Flutter Web beta,结果连渐变组件都还没移植上去,之前想用 Flutter 做跨平台软件的想法看来还是要搁置一段时间。


做 Flutter Clock 的时候想起来以前 iOS 上有很多漂亮的天气软件,比如 SolarSun,不只是在视觉上很棒,在交互上也很有想法,不像现在的软件全都是 Large Title + Navigation Bar。可惜这两个一个太过久远 API 已经挂了,一个直接被开发者下架。有的时候就会感叹,这种精致的软件,特别是依赖网络 API 的,对后世的影响力连一个十年都不到,2020 年大家还能听 1980 年的音乐,却已经连 2012 年的软件都再也运行不起来了。


听了 FE Radio 131 + More//NightPoetic Justice (Res Edit)PYRMDPLAZA (Joe Kay's Slowed Edit),看了 Best of Between the Scenes 2019 | The Daily Show 以及 超級歪【20萬訂閱Q&A】

2019.12.24

 • 

去拔了牙。打了麻醉又掉了牙齿,因此现在特别困,准备去睡了。
留学的第一批申请递交出去了,第二批还在准备,又是比较赶的安排了。
在 Xbox 上买了 Elite Dangerous,结果和 Steam 版本一样的 bug 众多,而且不能同步之前在 Steam 上的账户,因此玩了两小时就弃坑了。
听了 Soulection Radio 437SPACY。看了 The Illusion of Peace in Mamoru Oshii's Patlabor 2,最后把 World War II in Colour 也追完了。

2019.12.16

 • 

出了 18 年 3 月 Pi Day 买的 Alienware 15 R3,黑五的时候转运了 MSI P65,结果 UPS 给我送偏了,外加转运中国目前只能走 USPS 目的地清关,加起来时间和价格上与亚马逊海外购都不占优了。


为了申请学校更新了 Portfolio,加上了 Air Ink。这几天还会把之前许多 private 的仓库给 public 出来装点下 GitHub 门面,毕竟自己 star 比较多的几个仓库都是四年前写的,现在看惨不忍睹。也用 LaTeX 写了一份学术性质的 CV,为了防止教授们对 Justin Fincher 这个网络 ID 和 fincher.im 这个域名有疑问之前还买了一个新的中文拼音的域名做了重定向。


申请季自己的心态也有些变化。之前看到各类新 iOS 或者 macOS 的 concept video 都是嗤之以鼻,觉得大部分都经不起推敲,设计上也很刻意,总而言之没有能打的(反倒是有一些 Windows 的 redesign 项目我觉得还不错,当然也许只是流行度导致的样本总数不同导致的偏差)。但是自己又没有要去自己做一个的意思,时间不够,另外隐隐也觉得没有什么意义,毕竟一来不是行家二来没有弄清楚到底我做这个有什么好处。总之就是外人看起来比较狂妄又没有作品的感觉,因此我也没有对外提起过我的看法。不过申请季的时候突然觉得管他呢,找个周末当 hackathon 一样突击做出来,最后出成品至少也是一个能看的,对个人资料的丰富和 personal branding 都是有好处的,干嘛要担心那么多。


之前和猫猫说,负责托管她的简历的那台 VPS 只开了一个 nginx 每天无所事事的感觉,月终还扣我全款,不如拿来做些好玩的事情。因此目前属于她的那台服务器已经挂上了 BOINC 在以 70% 的占用率跑 SETI@Home,猫猫也觉得挺开心。


听了 SoundCloud 上的 2019 个人前三十 和 Spotify 上的 2019 个人前一百

2019.12.1

 • 

没想到都十二月了,过的真快。
自己的选校和文书都还没准备好,稍微有点迟了,然而还是在这里面对着电脑敲博客。
十一月底又去了东京一趟,作为 Inkathon 七个队伍其中一个队的 Session Speaker 参加了 Connected Ink,展示了下利用 Mirror 跨设备同步 Skinned Mesh 的改进版 Air Ink。这次的 Session 包括了 Tokyo 和 Portland 两场 Inkathon 的队伍,聊起来也挺愉快。结束了 Connected Ink 后面基了宅撩Maple,吃了好吃的烤肉。第二天睡到中午然后出去逛,然后第三天也就是前天回来了。
然后第四天也就是昨天上午去考了场 GRE,这场 GRE 完全是水过去的,原因是我本身 GRE 分数已经差不多够了不需要再刷但是又过了退考时间,因此想着那就考下罢,结果又飞机又火车的奔波导致考试的时候特别困,考试的时候特别后悔,想着睡觉或者研究黑五可比做题目舒服多了。不过神奇的是分数竟然没怎么掉,但是也没什么用了 hhh。

airink

Air Ink 的展位,下午的 Session 到来之前就在这里负责展示,不过 Air Ink 项目即使改进了一些后仍然是一个非常前卫的项目,而我这里说的前卫是指“好玩”但是“仍然没有具体的前景”,因此我们的展位人数相对少了很多。左边的 keynote 其实是用 Deckset 导出图片序列后塞进去的,我发现 Deckset 真是懒人必备。

fuji

富士山。在日本的前两天都在间歇下雨,只有临走的当天天气极好,因此在飞机上拍到了。

carbook

台场某个杂货店的二楼买的 Automobile Year 94/95 版,那里非常好玩,全部都是关于汽车的老旧书籍,甚至有某某越野车的维修指南,而且非日文的外文书半价,因此购得一本。

2019.11.24

 • 

前几天得知 GRE 的写作考试分数出来了,就此 GRE 也不用考了(虽然还有一场过了退考时间的可以月底再刷一次),只用准备文书了。
项目有一堆要修的,比如这个博客的主题也要找个时间修整下,Skyline 的手势识别还是没修,原本计划十一月能上架 Asset Store 的 Unity Android LWP 方案因为 2019.3 导出 gradle 项目结构的改变也要修改了。不过 2019.3 这一改倒是对开发方便了很多,可以直接把 Unity 当 lib 来管理了,只不过最近 Unity 的其他举措都有些迷,比如 LWRP 都要 production ready 了突然宣布 Deprecation 然后官方博客说只是把 LWRP rebrand 到 URP,但真正迁移了后才发现着色器的 namespace 变了,Post Processing V2 也不支持了,各种鸡飞狗跳,我自己的 Metropolis 项目因此彻底迁移回默认管线了,Skyline 暂时留着 6.x 的 LWRP 不动先。不知道再这么弄下去 Asset Store 的素材更新会不会因此断档一段时间,这对素材发布者和用 Unity 的开发者都是个很劝退的事情,至少对于我来说,用 Unity 的一大要素就是 Asset Store 东西很全,不会某项技能可以直接无脑买了即插即用,而如果 pipeline 三天两头出毫无意义的 break changes 导致上游的素材发布者不兼容甚至弃坑接着我也把我的项目弃坑或者直接去用 Godot 或者 Unreal 了,最后损害的反倒是 Unity。
最近听了 Summer Breeze(Piper 在 Spotify 港区,Apple Music 和 Tidal 上都没,最后还是只能用虾米,但说实话虾米被阿里买了之后现在已经特别恶心了),Soulection 434UnwindTemptation,当然还有 FKJ 新专 Ylang Ylang,虽然这张里面没有我想要的 Red Flowers,或许下张会收录吧。
总之,这个十一月大都在处理语言考试或者处理文档,寥寥无几(其实也有快一周啦)写代码的时间学了怎么用 Mirror,希望下个月能早点结束文书和选校然后我能去开始做跳票了快三年的 Epoch

2019.11.7

 • 

IMG_20191104_234719
去东京参加了 Wacom Inkathon 的 Final,到场其他人的项目基本上都是 production ready 了,自己的半成品自然是没有什么结果 hhhh,但是各个队伍都很友好,交流的很愉快,以及后期也许还有协作的可能。总之在东京的这几天真的非常开心,除了还是很困以外。

Google Sheet + Apps Script + Glide

 • 

背 GRE 单词的时候弄的一套自动化流程,可以批量查词修改 Spreadsheet,配合 GlideApp 生成 PWA 在手机上用,效果可以参考这里

1) 导入 Apps Script

这个脚本基本上就两个功能,一个是触发器,负责在修改文档的时候更新那一行的内容,第二个是一些数值函数,可以直接在 Sheet 的 f(x) 框里面用。所有查询的词都做了 lowercase 和 cache,毕竟 UrlFetchApp 有配额一天只能跑上几千次,要省着点用,而且如果用了 f(x) 表达式的话,每次打开页面都请求爱词霸也挺拖累别人服务器的。

appsscript.json

{
  "timeZone": "Asia/Hong_Kong",
  "dependencies": {
  },
  "webapp": {
    "access": "ANYONE",
    "executeAs": "USER_ACCESSING"
  },
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": ["https://www.googleapis.com/auth/documents", "https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/spreadsheets"]
}

code.gs

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Words')
      .addItem('Reset Cache', 'ResetCache')
      .addToUi();
}

function onEditInstalledTrigger(e) {
  var range = e.range;
  var sheet = SpreadsheetApp.getActiveSheet();
  
  Logger.log("row = " + range.getRow() + " column = " + range.getColumn);
  Logger.log("row Number = " + range.getNumRows() + " column Number = " + range.getNumColumns());
  
  for (var i = range.getRow(); i <= range.getRow() + range.getNumRows() - 1; i++) {
    for (var j = range.getColumn(); j <= range.getColumn() + range.getNumColumns() - 1; j++) 
    {
      if (j == 1)
      {
        var currentValue = sheet.getRange(i,j).getValue();
        var wordLowCase = GetWordLowerCase(currentValue);
        var empty = IsEmpty(wordLowCase);
        var cache = IsCached(wordLowCase);
        
        Logger.log("row i = " + i + " column j = " + i + " word = " + wordLowCase);
        var attribute = {
          cache:cache,
          value:wordLowCase,
          empty:empty
        }
        sheet.getRange(i,j).setNote(empty ? "" : JSON.stringify(attribute));
        sheet.getRange(i,j+1).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+2).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+3).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+4).setValue(empty ? "" : "LOADING");
        sheet.getRange(i,j+5).setValue(empty ? "" : "LOADING");
        
        var wordJsonObject = empty ? {} : GetWord(wordLowCase);
        attribute.json = wordJsonObject;
        sheet.getRange(i,j).setNote(empty ? "" : JSON.stringify(attribute));
        Logger.log(JSON.stringify(attribute));
        
        if (wordJsonObject.status == 0)
        {
          var wordMeaning = empty ? "" : wordJsonObject.content.word_mean.join("\r\n");
          sheet.getRange(i,j+1).setValue(wordMeaning);
          
          var worldPronunciationUS = empty ? "" : "/" + wordJsonObject.content.ph_am + "/";
          sheet.getRange(i,j+2).setValue(worldPronunciationUS);
          
          var worldPronunciationURLUS = empty ? "" : wordJsonObject.content.ph_am_mp3;
          sheet.getRange(i,j+3).setValue(worldPronunciationURLUS);
          
          var cachedString = empty ? "" : (cache ? "YES" : "NO");
          sheet.getRange(i,j+4).setValue(cachedString);
      
        }
        
        var updateTime = empty ? "" : new Date().toISOString();
        sheet.getRange(i,j+5).setValue(updateTime);
       
      }
    }
  }
}

function ResetCache() {
  var cache = CacheService.getScriptCache();
  var selection = SpreadsheetApp.getActiveSheet();
  var data = selection.getSelection().getActiveRange().getValues()
  for (var i = 0; i < data.length; i++) {
    var wordLowCase = GetWordLowerCase(data[i][0]);
    Logger.log('Cache remove: ' + wordLowCase);
    cache.remove(wordLowCase);
  }
}

function GetWordLowerCase(word)
{
  return String(word).toLowerCase();
}

function GetWordUrl(word)
{
  var wordLowCase = GetWordLowerCase(word);
  var url = "http://fy.iciba.com/ajax.php?a=fy&f=auto&t=zh&w=".concat(encodeURIComponent(wordLowCase));
  return url;
}

function IsCached(word)
{
  if (IsEmpty(word))
  {
    return false;
  }
  var wordLowCase = GetWordLowerCase(word);
  var cache = CacheService.getScriptCache();
  var cached = cache.get(wordLowCase);
  return cached != null;
}

function IsEmpty(str) {
    return (!str || 0 === str.length);
}

/**
 * Get Word JSON Object.
 *
 * @param {word} the word.
 * @return JSON returned from iciba.
 * @customfunction
 */
function GetWord(word) {
  if (IsEmpty(word))
  {
    Logger.log("word == null");
    return {};
  }
  var wordLowCase = GetWordLowerCase(word);
  var cache = CacheService.getScriptCache();
  var cached = cache.get(wordLowCase);
  if (cached != null) {
    return JSON.parse(cached);
  }
  var url = GetWordUrl(wordLowCase);
  var jsonData = UrlFetchApp.fetch(url);
  var jsonContent = jsonData.getContentText();
  cache.put(wordLowCase, jsonContent, 604800); // cache for random minutes
  var object   = JSON.parse(jsonContent);
  return object;
}
/**
 * @customfunction
 */
function GetWordMeaning(word)
{
  var object = GetWord(word);
  return object.content.word_mean.join("\r\n");
}
/**
 * @customfunction
 */
function GetWordPronunciationUS(word)
{
  var object = GetWord(word);
  return object.content.ph_am;
}
/**
 * @customfunction
 */
function GetWordPronunciationUSURL(word)
{
  var object = GetWord(word);
  return object.content.ph_am_mp3;
}
/**
 * @customfunction
 */
function GetWordJSON(word)
{
  var object = GetWord(word);
  return JSON.stringify(object);
}

然后部署下,允许下文件访问

2) 安装 Trigger

表格 - 工具 - 脚本触发器 - 修改 - 当前项目触发器 - 添加:

  • 选择要运行的功能 OnEditInstalledTrigger
  • 选择活动来源 基于电子表格
  • 选择活动来源 编辑时
3) 输入数据

第一行最好 freeze 住,然后在第二行开始输入英文,Trigger 会自动补全后面几列
Screenshot-2019-10-24-at-12.44.30-AM

4) 同步到 Glide 里

Glide 里新建一个项目绑定这个表格。
Glide 比较好的地方在于,他们的 Detail Page 可以设定 Audio 组件绑定 URL,刚好可以对应爱词霸 API 里的音标地址,基本配置完可以当一个自定义单词本来用了。

Screenshot-2019-10-24-at-12.49.27-AM-2
Screenshot-2019-10-24-at-12.50.28-AM

2019.10.21

 • 

Hackathon 入围第一轮了,接下来下个月会去东京参加 Final。自己的作品原本就只是三四天完成的,因此选题和实现上都是典型的求快的方式,在长期改进方面相比其他队伍就会比较被动,加上自己还有语言考试没有结束,只能抽时间做了。
回家休息了一周,心态懒散了不少。
看了 Hayao Miyazaki's AirshipsTrevor Noah: Afraid of the Dark